From 2089170dcb933a73fe0d45ec6a130948d1c8045e Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 20 Jun 2023 12:53:11 +0100 Subject: [PATCH 001/208] 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 002/208] 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 003/208] 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 004/208] 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 005/208] 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 006/208] 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 007/208] 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 008/208] 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 009/208] 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 010/208] 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 011/208] 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 012/208] 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 013/208] 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 014/208] 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 015/208] 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 016/208] 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 017/208] 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 018/208] 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 019/208] 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 020/208] 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 021/208] 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 022/208] 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 023/208] 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 024/208] 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 025/208] 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 026/208] 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 027/208] 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 028/208] 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 029/208] 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 030/208] 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 031/208] 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 032/208] 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 033/208] 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 034/208] 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 035/208] 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 036/208] 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 037/208] 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 038/208] 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 039/208] 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 040/208] 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 041/208] 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 042/208] 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 043/208] 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 044/208] 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 045/208] 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 046/208] 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 047/208] 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 048/208] 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 049/208] 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 050/208] 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 051/208] 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 052/208] 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 053/208] 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 054/208] 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 055/208] 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 056/208] 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 057/208] 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 058/208] 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 059/208] 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 060/208] 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 061/208] 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 062/208] 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 063/208] 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 064/208] 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 065/208] 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 066/208] 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 067/208] 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 068/208] 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 069/208] 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 070/208] 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 071/208] 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 072/208] 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 073/208] 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 074/208] 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 075/208] 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 076/208] 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 077/208] 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 078/208] 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 079/208] 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 080/208] 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 081/208] 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 082/208] 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 083/208] 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 084/208] 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 085/208] 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 086/208] 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 087/208] 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 088/208] 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 089/208] 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 090/208] 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 091/208] 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 092/208] 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 093/208] 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 094/208] 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 095/208] 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 096/208] 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 097/208] 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) From fea10be5eb2f2ea39418f00e5430187c24ca6d73 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 16:51:25 +0100 Subject: [PATCH 098/208] refactor: test coverage in makefile --- Makefile | 7 +------ requirements.txt | 5 +++++ test_requirements.txt | 2 -- 3 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 test_requirements.txt diff --git a/Makefile b/Makefile index 59292f8..3c26230 100644 --- a/Makefile +++ b/Makefile @@ -65,11 +65,9 @@ dbclean: @find . -name "delete_db.py" -exec python3 {} \; format: - $(PIP) install black $(PYTHON) -m black . lint: - $(PIP) install flake8 pylint $(PYTHON) -m flake8 $(PYTHON) -m pylint **/*.py @@ -82,14 +80,11 @@ setup: build dbclean bids swag: open http://localhost:8080/api/docs/#/ -test: test_setup +test: 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/requirements.txt b/requirements.txt index 7772fce..42a3674 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,8 @@ flask_swagger_ui marshmallow pymongo python-dotenv +black +flake8 +pylint +coverage +pytest \ No newline at end of file diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index e36ca7a..0000000 --- a/test_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -coverage -pytest \ No newline at end of file From c41237113d040f9930dc86d020e23f959ecf3fa2 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 16:53:18 +0100 Subject: [PATCH 099/208] docs: readme.md containing details of scripts usage --- scripts/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scripts/README.md b/scripts/README.md index e69de29..4dfb52e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -0,0 +1,34 @@ +# MongoDB Database Cleanup and Sample Data Population Scripts + +## Database Cleanup Script + +### Script Description +- The delete_db.py script is used to delete all bids from the MongoDB collection. +- The create_sample_data.py script is used to populate the MongoDB database with sample bids data from the bids.json file. + +### Usage + +To run the database cleanup script, execute the following command: +```bash +python3 delete_db.py +``` + +To run the sample data population script, execute the following command: +```bash +python3 create_sample_data.py +``` + +## Application Setup + +### Script Description +The setup target in the Makefile sets up the application database by performing the following steps: + +1. Building the application using the build target. +2. Cleaning up the existing database using the dbclean target. +3. Creating sample data using the bids target. + +### Usage +To set up the application database, run the following command: +```bash +make setup +``` \ No newline at end of file From ec484533508318baf0c072c29801fc6deb46b8c9 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 16:55:24 +0100 Subject: [PATCH 100/208] feat: flake8 config file created on root folder --- .flake8 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..34bb0c5 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 120 +exclude = .venv *.json + +ignore= F811,W503, \ No newline at end of file From 4169d6d7f76283226ed87670900d6041e00ce48c Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 16:56:42 +0100 Subject: [PATCH 101/208] feat: pylint config file created on root folder --- .pylintrc | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..6818271 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,7 @@ +[FORMAT] + +# Set the maximum line length to 100 characters +max-line-length=120 + +[MESSAGES CONTROL] +disable=C0103,W0613 \ No newline at end of file From 10ae67fcbd44708f8e89e7265ca36d6a6e55bf10 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 17:16:50 +0100 Subject: [PATCH 102/208] fix: lint error handeling exception bid_controler and except block helpers --- api/controllers/bid_controller.py | 4 ++-- helpers/helpers.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 38b87bd..7b54f6c 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -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 as e: + except Exception: return showInternalServerError(), 500 @@ -94,7 +94,7 @@ def update_bid_by_id(bid_id): except UnprocessableEntity as e: return showUnprocessableEntityError(e), 422 # Return 500 response in case of connection failure - except Exception as e: + except Exception: return showInternalServerError(), 500 diff --git a/helpers/helpers.py b/helpers/helpers.py index 436feec..c23a4a7 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -34,7 +34,7 @@ def is_valid_isoformat(string): try: datetime.fromisoformat(string) return True - except: + except ValueError: return False From 59657eb2d1f6d9fa2629e9044d13bf913d292b29 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 17:25:26 +0100 Subject: [PATCH 103/208] fix: lint C0411 wrong-import-order --- .pylintrc | 2 +- dbconfig/mongo_setup.py | 2 +- helpers/helpers.py | 2 +- scripts/create_sample_data.py | 4 ++-- scripts/delete_db.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6818271..732c990 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,4 +4,4 @@ max-line-length=120 [MESSAGES CONTROL] -disable=C0103,W0613 \ No newline at end of file +disable=C0103,W0613,R0801,C0116,C0114 \ No newline at end of file diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index b902276..d972f0f 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -1,5 +1,5 @@ -from pymongo import MongoClient import os +from pymongo import MongoClient from dotenv import load_dotenv load_dotenv() diff --git a/helpers/helpers.py b/helpers/helpers.py index c23a4a7..b102bec 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,6 +1,6 @@ -from flask import jsonify import uuid from datetime import datetime +from flask import jsonify from werkzeug.exceptions import UnprocessableEntity from api.schemas.bid_schema import BidSchema from api.schemas.bid_id_schema import BidIdSchema diff --git a/scripts/create_sample_data.py b/scripts/create_sample_data.py index 20365ef..612f776 100644 --- a/scripts/create_sample_data.py +++ b/scripts/create_sample_data.py @@ -4,10 +4,10 @@ """ -from pymongo import MongoClient -from dotenv import load_dotenv import os import json +from pymongo import MongoClient +from dotenv import load_dotenv load_dotenv() diff --git a/scripts/delete_db.py b/scripts/delete_db.py index e9c319b..98cd72e 100644 --- a/scripts/delete_db.py +++ b/scripts/delete_db.py @@ -3,8 +3,8 @@ """ -from pymongo import MongoClient import os +from pymongo import MongoClient from dotenv import load_dotenv load_dotenv() From b0673f6d8b8fe9e720103cc9ba21041f4074bbff Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 17:29:36 +0100 Subject: [PATCH 104/208] fix: lint W0718-R1722-W1514 create_sample_data --- scripts/create_sample_data.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/scripts/create_sample_data.py b/scripts/create_sample_data.py index 612f776..3b1fcb6 100644 --- a/scripts/create_sample_data.py +++ b/scripts/create_sample_data.py @@ -6,7 +6,9 @@ import os import json +import sys from pymongo import MongoClient +from pymongo.errors import ConnectionFailure from dotenv import load_dotenv load_dotenv() @@ -15,10 +17,13 @@ def populate_bids(): + """ + Populates the MongoDB database with sample bids data from bids.json file. + """ try: client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) - db = client["bidsAPI"] - collection = db["bids"] + data_base = client["bidsAPI"] + collection = data_base["bids"] # Get the current script's directory current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -27,7 +32,7 @@ def populate_bids(): file_path = os.path.join(current_dir, "test_data", "bids.json") # Read bids data from JSON file - with open(file_path) as bids_file: + with open(file_path, encoding="utf-8") as bids_file: bids_data = json.load(bids_file) # Insert bids into the database @@ -40,9 +45,9 @@ def populate_bids(): collection.insert_one(bid) print(f"Inserted bid with _id: {bid['_id']}") - except Exception as e: - print(f"Error: {e}") - exit(1) + except ConnectionFailure as error: + print(f"Error: {error}") + sys.exit(1) finally: # Close the MongoDB connection @@ -51,4 +56,4 @@ def populate_bids(): if __name__ == "__main__": populate_bids() - exit(0) + sys.exit(0) From 58bcb6969ed34e7924f91cdc611fba80b796a3c6 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 17:31:43 +0100 Subject: [PATCH 105/208] fix: lint W0718,R1722 delete_db --- scripts/delete_db.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/delete_db.py b/scripts/delete_db.py index 98cd72e..a36efa0 100644 --- a/scripts/delete_db.py +++ b/scripts/delete_db.py @@ -4,7 +4,9 @@ """ import os +import sys from pymongo import MongoClient +from pymongo.errors import ConnectionFailure from dotenv import load_dotenv load_dotenv() @@ -13,22 +15,24 @@ def delete_bids(): + """ + Deletes all bids from the MongoDB collection. + """ try: client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) - db = client["bidsAPI"] - collection = db["bids"] + data_base = client["bidsAPI"] + collection = data_base["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) + except ConnectionFailure as error: + print(f"Error: {error}") + sys.exit(1) finally: client.close() @@ -36,4 +40,4 @@ def delete_bids(): if __name__ == "__main__": delete_bids() - exit(0) + sys.exit(0) From d9a8d69581e9853e092f775d582e5d32f985191a Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 18:06:08 +0100 Subject: [PATCH 106/208] fix: lint W0621 Redefining name 'client' to 'test_client' --- tests/conftest.py | 2 +- tests/test_delete_bid.py | 16 ++++++++-------- tests/test_get_bid_by_id.py | 16 ++++++++-------- tests/test_get_bids.py | 12 ++++++------ tests/test_phase_schema.py | 8 ++++---- tests/test_post_bid.py | 12 ++++++------ tests/test_update_bid_by_id.py | 28 ++++++++++++++-------------- tests/test_update_bid_status.py | 20 ++++++++++---------- 8 files changed, 57 insertions(+), 57 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8aa7aa7..25e8539 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ @pytest.fixture -def client(): +def test_client(): app = Flask(__name__) app.register_blueprint(bid, url_prefix="/api") with app.test_client() as client: diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index b08dffa..3e2798f 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -3,39 +3,39 @@ # Case 1: Successful delete a bid by changing status to deleted @patch("api.controllers.bid_controller.db") -def test_delete_bid_success(mock_db, client): +def test_delete_bid_success(mock_db, test_client): 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") + response = test_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.db") -def test_delete_bid_connection_error(mock_db, client): +def test_delete_bid_connection_error(mock_db, test_client): mock_db["bids"].find_one_and_update.side_effect = Exception - response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + response = test_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.db") -def test_delete_bid_validation_error(mock_db, client): - response = client.delete("/api/bids/invalid_bid_id") +def test_delete_bid_validation_error(mock_db, test_client): + response = test_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.db") -def test_delete_bid_not_found(mock_db, client): +def test_delete_bid_not_found(mock_db, test_client): mock_db["bids"].find_one_and_update.return_value = None - response = client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + response = test_client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") mock_db["bids"].find_one_and_update.assert_called_once() assert response.status_code == 404 diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index f1398e9..c4e68aa 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -3,7 +3,7 @@ # Case 1: Successful get_bid_by_id @patch("api.controllers.bid_controller.db") -def test_get_bid_by_id_success(mock_db, client): +def test_get_bid_by_id_success(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "alias": "ONS", @@ -19,7 +19,7 @@ def test_get_bid_by_id_success(mock_db, client): "was_successful": False, } - response = client.get( + response = test_client.get( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", headers={"host": "localhost:8080"}, ) @@ -46,19 +46,19 @@ def test_get_bid_by_id_success(mock_db, client): # Case 2: Connection error @patch("api.controllers.bid_controller.db", side_effect=Exception) -def test_get_bid_by_id_connection_error(mock_db, client): +def test_get_bid_by_id_connection_error(mock_db, test_client): mock_db["bids"].find_one.side_effect = Exception - response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + response = test_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.db") -def test_get_bid_by_id_not_found(mock_db, client): +def test_get_bid_by_id_not_found(mock_db, test_client): mock_db["bids"].find_one.return_value = None - response = client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + response = test_client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") mock_db["bids"].find_one.assert_called_once_with( {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": {"$ne": "deleted"}} @@ -69,7 +69,7 @@ def test_get_bid_by_id_not_found(mock_db, client): # Case 4: Validation error @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") +def test_get_bid_by_id_validation_error(mock_db, test_client): + response = test_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 dabd495..414fe4b 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -3,10 +3,10 @@ # Case 1: Successful get @patch("api.controllers.bid_controller.db") -def test_get_bids_success(mock_db, client): +def test_get_bids_success(mock_db, test_client): mock_db["bids"].find.return_value = [] - response = client.get("/api/bids") + response = test_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": []} @@ -14,7 +14,7 @@ def test_get_bids_success(mock_db, client): # Case 2: Links prepended with hostname @patch("api.controllers.bid_controller.db") -def test_links_with_host(mock_db, client): +def test_links_with_host(mock_db, test_client): mock_db["bids"].find.return_value = [ { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", @@ -29,7 +29,7 @@ def test_links_with_host(mock_db, client): } ] - response = client.get("/api/bids", headers={"host": "localhost:8080"}) + response = test_client.get("/api/bids", headers={"host": "localhost:8080"}) assert response.status_code == 200 assert response.get_json() == { "total_count": 1, @@ -51,8 +51,8 @@ def test_links_with_host(mock_db, client): # Case 3: Connection error @patch("api.controllers.bid_controller.db") -def test_get_bids_connection_error(mock_db, client): +def test_get_bids_connection_error(mock_db, test_client): mock_db["bids"].find.side_effect = Exception - response = client.get("/api/bids") + response = test_client.get("/api/bids") assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} diff --git a/tests/test_phase_schema.py b/tests/test_phase_schema.py index 085367e..ee85177 100644 --- a/tests/test_phase_schema.py +++ b/tests/test_phase_schema.py @@ -3,7 +3,7 @@ # Case 1: score is mandatory when has_score is set to True @patch("api.controllers.bid_controller.db") -def test_score_is_mandatory(mock_db, client): +def test_score_is_mandatory(mock_db, test_client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -11,7 +11,7 @@ def test_score_is_mandatory(mock_db, client): "success": [{"phase": 1, "has_score": True, "out_of": 36}], } - response = client.post("api/bids", json=data) + response = test_client.post("api/bids", json=data) assert response.status_code == 400 assert ( response.get_json()["Error"] @@ -21,7 +21,7 @@ def test_score_is_mandatory(mock_db, client): # Case 2: out_of is mandatory when has_score is set to True @patch("api.controllers.bid_controller.db") -def test_out_of_is_mandatory(mock_db, client): +def test_out_of_is_mandatory(mock_db, test_client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -29,7 +29,7 @@ def test_out_of_is_mandatory(mock_db, client): "failed": {"phase": 2, "has_score": True, "score": 20}, } - response = client.post("api/bids", json=data) + response = test_client.post("api/bids", json=data) assert response.status_code == 400 assert ( response.get_json()["Error"] diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 6a3227f..020750b 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -3,7 +3,7 @@ # Case 1: Successful post @patch("api.controllers.bid_controller.db") -def test_post_is_successful(mock_db, client): +def test_post_is_successful(mock_db, test_client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -21,7 +21,7 @@ def test_post_is_successful(mock_db, client): # Mock the behavior of db mock_db["bids"].insert_one.return_value = data - response = client.post("api/bids", json=data) + response = test_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 ( @@ -45,10 +45,10 @@ def test_post_is_successful(mock_db, client): # Case 2: Missing mandatory fields @patch("api.controllers.bid_controller.db") -def test_field_missing(mock_db, client): +def test_field_missing(mock_db, test_client): data = {"client": "Sample Client", "bid_date": "2023-06-20"} - response = client.post("api/bids", json=data) + response = test_client.post("api/bids", json=data) assert response.status_code == 400 assert response.get_json() == { "Error": "{'tender': {'message': 'Missing mandatory field'}}" @@ -57,7 +57,7 @@ def test_field_missing(mock_db, client): # Case 3: Connection error @patch("api.controllers.bid_controller.db") -def test_post_bid_connection_error(mock_db, client): +def test_post_bid_connection_error(mock_db, test_client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -73,7 +73,7 @@ def test_post_bid_connection_error(mock_db, client): } # Mock the behavior of db mock_db["bids"].insert_one.side_effect = Exception - response = client.post("/api/bids", json=data) + response = test_client.post("/api/bids", json=data) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index 130c3e6..ad824fb 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -3,7 +3,7 @@ # Case 1: Successful update @patch("api.controllers.bid_controller.db") -def test_update_bid_by_id_success(mock_db, client): +def test_update_bid_by_id_success(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", @@ -16,7 +16,7 @@ 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) + response = test_client.put(f"api/bids/{bid_id}", json=update) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} ) @@ -26,7 +26,7 @@ def test_update_bid_by_id_success(mock_db, client): # Case 2: Invalid user input @patch("api.controllers.bid_controller.db") -def test_input_validation(mock_db, client): +def test_input_validation(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -47,35 +47,35 @@ def test_input_validation(mock_db, client): } bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"tender": 42} - response = client.put(f"api/bids/{bid_id}", json=update) + response = test_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.db") -def test_bid_not_found(mock_db, client): +def test_bid_not_found(mock_db, test_client): 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) + response = test_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.db") -def test_cannot_update_status(mock_db, client): +def test_cannot_update_status(mock_db, test_client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "deleted"} - response = client.put(f"api/bids/{bid_id}", json=update) + response = test_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.db") -def test_update_by_id_find_error(mock_db, client): +def test_update_by_id_find_error(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", @@ -88,14 +88,14 @@ def test_update_by_id_find_error(mock_db, client): 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) + response = test_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): +def test_update_failed(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -116,7 +116,7 @@ def test_update_failed(mock_db, client): } 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) + response = test_client.put(f"api/bids/{bid_id}", json=update) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} ) @@ -126,7 +126,7 @@ def test_update_failed(mock_db, client): # Case 7: Update success field @patch("api.controllers.bid_controller.db") -def test_update_success(mock_db, client): +def test_update_success(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -152,7 +152,7 @@ def test_update_success(mock_db, client): {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, ] } - response = client.put(f"api/bids/{bid_id}", json=update) + response = test_client.put(f"api/bids/{bid_id}", json=update) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} ) diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index 3ee6513..b8aec2f 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -3,7 +3,7 @@ # Case 1: Successful update @patch("api.controllers.bid_controller.db") -def test_update_bid_status_success(mock_db, client): +def test_update_bid_status_success(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -25,7 +25,7 @@ def test_update_bid_status_success(mock_db, client): bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"status": "completed"} - response = client.put(f"api/bids/{bid_id}/status", json=update) + response = test_client.put(f"api/bids/{bid_id}/status", json=update) 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 @@ -33,7 +33,7 @@ def test_update_bid_status_success(mock_db, client): # Case 2: Invalid status @patch("api.controllers.bid_controller.db") -def test_invalid_status(mock_db, client): +def test_invalid_status(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -54,7 +54,7 @@ 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) + response = test_client.put(f"api/bids/{bid_id}/status", json=update) assert response.status_code == 400 assert ( response.get_json()["Error"] @@ -64,31 +64,31 @@ def test_invalid_status(mock_db, client): # Case 3: Empty request body @patch("api.controllers.bid_controller.db") -def test_empty_request(mock_db, client): +def test_empty_request(mock_db, test_client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {} - response = client.put(f"api/bids/{bid_id}/status", json=update) + response = test_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.db") -def test_bid_not_found(mock_db, client): +def test_bid_not_found(mock_db, test_client): 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) + response = test_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.db") -def test_update_status_find_error(mock_db, client): +def test_update_status_find_error(mock_db, test_client): 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) + response = test_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 33bf1b9646436e5133be89064469277bf7c2b87b Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 18:31:50 +0100 Subject: [PATCH 107/208] fix: lint C0411-W0622 and update makefile --- .pylintrc | 2 +- Makefile | 2 +- api/controllers/bid_controller.py | 2 +- api/models/bid_model.py | 2 +- api/models/links_model.py | 6 +++--- api/schemas/bid_schema.py | 2 +- api/schemas/phase_schema.py | 2 +- helpers/__init__.py | 0 tests/test_bid_schema.py | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 helpers/__init__.py diff --git a/.pylintrc b/.pylintrc index 732c990..69de8b0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,4 +4,4 @@ max-line-length=120 [MESSAGES CONTROL] -disable=C0103,W0613,R0801,C0116,C0114 \ No newline at end of file +disable=C0103,W0613,R0801,C0116,C0114,C0115 \ No newline at end of file diff --git a/Makefile b/Makefile index 3c26230..517ae87 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,7 @@ format: lint: $(PYTHON) -m flake8 - $(PYTHON) -m pylint **/*.py + $(PYTHON) -m pylint **/*.py **/**/*.py *.py run: build $(PYTHON) app.py diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 7b54f6c..e9f0591 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,5 +1,5 @@ -from flask import Blueprint, request from datetime import datetime +from flask import Blueprint, request from marshmallow import ValidationError from werkzeug.exceptions import UnprocessableEntity from api.models.status_enum import Status diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 442b7a8..8910f6a 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -1,7 +1,7 @@ from uuid import uuid4 from datetime import datetime -from .links_model import LinksModel from api.models.status_enum import Status +from .links_model import LinksModel # Description: Schema for the bid object diff --git a/api/models/links_model.py b/api/models/links_model.py index 4a3535b..1157e13 100644 --- a/api/models/links_model.py +++ b/api/models/links_model.py @@ -1,5 +1,5 @@ # Schema for links object class LinksModel: - def __init__(self, id): - self.self = f"/bids/{id}" - self.questions = f"/bids/{id}/questions" + def __init__(self, bid_id): + self.self = f"/bids/{bid_id}" + self.questions = f"/bids/{bid_id}/questions" diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 67a7f1e..6de59ed 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,9 +1,9 @@ from marshmallow import Schema, fields, post_load, validates_schema, ValidationError +from api.models.status_enum import Status 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 diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index 259a437..8ef4774 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,5 +1,5 @@ -from marshmallow import Schema, fields, validates_schema, ValidationError from enum import Enum, unique +from marshmallow import Schema, fields, validates_schema, ValidationError @unique diff --git a/helpers/__init__.py b/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_bid_schema.py b/tests/test_bid_schema.py index 01d686b..a332c67 100644 --- a/tests/test_bid_schema.py +++ b/tests/test_bid_schema.py @@ -22,19 +22,19 @@ def test_bid_model(): bid_document = BidSchema().load(data) to_post = BidSchema().dump(bid_document) - id = to_post["_id"] + bid_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 + assert is_valid_uuid(bid_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 to_post["links"]["self"] == f"/bids/{bid_id}" assert "questions" in to_post["links"] - assert to_post["links"]["questions"] == f"/bids/{id}/questions" + assert to_post["links"]["questions"] == f"/bids/{bid_id}/questions" # Test that status is set to in_progress assert to_post["status"] == "in_progress" From cc3c51fdcd6998dc687dbbef80891fefaa91e0e8 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 19:28:05 +0100 Subject: [PATCH 108/208] fix: lint C0115 Missing class docstring --- .pylintrc | 2 +- api/models/bid_model.py | 18 +++++++++++++++++- api/models/links_model.py | 8 ++++++++ api/models/status_enum.py | 12 +++++++++++- api/schemas/bid_id_schema.py | 7 +++++++ api/schemas/bid_schema.py | 19 +++++++++++++++++++ api/schemas/feedback_schema.py | 8 ++++++++ api/schemas/links_schema.py | 8 ++++++++ api/schemas/phase_schema.py | 18 ++++++++++++++++++ 9 files changed, 97 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index 69de8b0..b0aba9d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,4 +4,4 @@ max-line-length=120 [MESSAGES CONTROL] -disable=C0103,W0613,R0801,C0116,C0114,C0115 \ No newline at end of file +disable=C0103,W0613,R0801,C0116,C0114,W0718,R0903,R0913,R0902 \ No newline at end of file diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 8910f6a..9be1259 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -6,6 +6,22 @@ # Description: Schema for the bid object class BidModel: + """ + Represents a bid model for the MongoDB database. + + Attributes: + tender (str): The tender for which the bid is submitted. + client (str): The client for whom the bid is prepared. + bid_date (str): The date when the bid was submitted (in ISO format). + alias (str): An alias or abbreviation for the client. + bid_folder_url (str): The URL to the bid's folder in the organization's SharePoint. + feedback (dict): A dictionary containing feedback information. + success (list): A list of dictionaries representing successful phases of the bid. + failed (dict): A dictionary representing the failed phase of the bid. + links (dict): A dictionary containing links to the bid resource and questions resource. + last_updated (str): The date and time when the bid was last updated (in ISO format). + """ + def __init__( self, tender, @@ -16,7 +32,7 @@ def __init__( feedback=None, failed=None, was_successful=False, - success=[], + success=None, status=None, _id=None, links=None, diff --git a/api/models/links_model.py b/api/models/links_model.py index 1157e13..91f4eec 100644 --- a/api/models/links_model.py +++ b/api/models/links_model.py @@ -1,5 +1,13 @@ # Schema for links object class LinksModel: + """ + Represents a links model for the bid resource. + + Attributes: + self (str): The URL to the bid resource. + questions (str): The URL to the questions resource related to the bid. + """ + def __init__(self, bid_id): self.self = f"/bids/{bid_id}" self.questions = f"/bids/{bid_id}/questions" diff --git a/api/models/status_enum.py b/api/models/status_enum.py index 6fa7dd5..54e88fa 100644 --- a/api/models/status_enum.py +++ b/api/models/status_enum.py @@ -1,9 +1,19 @@ from enum import Enum, unique -# Enum for status @unique class Status(Enum): + """ + Enumeration representing the status of a bid. + + Each status value represents a different state of a bid in the system. + + Enum Values: + DELETED (str): The bid has been deleted. + IN_PROGRESS (str): The bid is currently in progress. + COMPLETED (str): The bid has been completed. + """ + DELETED = "deleted" IN_PROGRESS = "in_progress" COMPLETED = "completed" diff --git a/api/schemas/bid_id_schema.py b/api/schemas/bid_id_schema.py index ab61b42..291e698 100644 --- a/api/schemas/bid_id_schema.py +++ b/api/schemas/bid_id_schema.py @@ -2,6 +2,13 @@ class BidIdSchema(Schema): + """ + Schema for validating bid IDs. + + Attributes: + bid_id (str): The bid ID to be validated. + """ + bid_id = fields.Str( required=True, validate=validate.Length(min=36, error="Invalid bid Id") ) diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 6de59ed..26467b8 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -8,6 +8,25 @@ # Marshmallow schema for request body class BidSchema(Schema): + """ + Marshmallow schema for the bid object. + + Attributes: + _id (UUID): The unique identifier of the bid. + tender (str): The tender for which the bid is submitted. + client (str): The client for whom the bid is prepared. + bid_date (Date): The date when the bid was submitted (in the format "%Y-%m-%d"). + alias (str, optional): An alias or abbreviation for the client. + bid_folder_url (str, optional): The URL to the bid's folder in the organization's SharePoint. + was_successful (bool, optional): Whether the bid was successful or not. + success (list of PhaseSchema, optional): A list of successful phases of the bid. + failed (PhaseSchema, optional): The failed phase of the bid. + feedback (FeedbackSchema, optional): Feedback information for the bid. + status (Status): The status of the bid (using the Status enum). + links (LinksSchema): Links to the bid resource and questions resource. + last_updated (DateTime, optional): The date and time when the bid was last updated. + """ + _id = fields.UUID() tender = fields.Str( required=True, diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py index a6aa08a..1c1809c 100644 --- a/api/schemas/feedback_schema.py +++ b/api/schemas/feedback_schema.py @@ -2,5 +2,13 @@ class FeedbackSchema(Schema): + """ + Schema for the feedback data in a bid. + + Attributes: + description (str): The description of the feedback. + url (str): The URL of the feedback. + """ + description = fields.Str(required=True) url = fields.URL(required=True) diff --git a/api/schemas/links_schema.py b/api/schemas/links_schema.py index 57bfacd..6d84ec1 100644 --- a/api/schemas/links_schema.py +++ b/api/schemas/links_schema.py @@ -2,5 +2,13 @@ class LinksSchema(Schema): + """ + Schema for representing links in a bid resource. + + Attributes: + self (str): The URL to the bid resource. + questions (str): The URL to the questions resource related to the bid. + """ + self = fields.Str(required=True) questions = fields.Str(required=True) diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index 8ef4774..8bcaaa8 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -4,11 +4,29 @@ @unique class Phase(Enum): + """ + Enum representing phases of a bid. + + Attributes: + PHASE_1 (int): Phase 1 of the bid. + PHASE_2 (int): Phase 2 of the bid. + """ + PHASE_1 = 1 PHASE_2 = 2 class PhaseSchema(Schema): + """ + Schema for representing a bid phase. + + Attributes: + phase (Phase): The phase of the bid. + has_score (bool): Indicates if the phase has a score. + score (int): The score of the phase (if applicable). + out_of (int): The maximum score possible for the phase (if applicable). + """ + phase = fields.Enum(Phase, required=True, by_value=True) has_score = fields.Boolean(required=True) score = fields.Integer() From 9f1a857e6163f66a3701532becc3619643ab4d9c Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 24 Jul 2023 19:59:16 +0100 Subject: [PATCH 109/208] feat: html docs with sphinx and string-comments --- {tests => api}/__init__.py | 0 docs/Makefile | 20 ++++++++++++++ docs/api.controllers.rst | 21 +++++++++++++++ docs/api.models.rst | 37 ++++++++++++++++++++++++++ docs/api.rst | 20 ++++++++++++++ docs/api.schemas.rst | 53 ++++++++++++++++++++++++++++++++++++++ docs/app.rst | 7 +++++ docs/conf.py | 31 ++++++++++++++++++++++ docs/dbconfig.rst | 21 +++++++++++++++ docs/helpers.rst | 21 +++++++++++++++ docs/index.rst | 20 ++++++++++++++ docs/make.bat | 35 +++++++++++++++++++++++++ docs/modules.rst | 12 +++++++++ requirements.txt | 4 ++- 14 files changed, 301 insertions(+), 1 deletion(-) rename {tests => api}/__init__.py (100%) create mode 100644 docs/Makefile create mode 100644 docs/api.controllers.rst create mode 100644 docs/api.models.rst create mode 100644 docs/api.rst create mode 100644 docs/api.schemas.rst create mode 100644 docs/app.rst create mode 100644 docs/conf.py create mode 100644 docs/dbconfig.rst create mode 100644 docs/helpers.rst create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/modules.rst diff --git a/tests/__init__.py b/api/__init__.py similarity index 100% rename from tests/__init__.py rename to api/__init__.py diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api.controllers.rst b/docs/api.controllers.rst new file mode 100644 index 0000000..5f85aa7 --- /dev/null +++ b/docs/api.controllers.rst @@ -0,0 +1,21 @@ +api.controllers package +======================= + +Submodules +---------- + +api.controllers.bid\_controller module +-------------------------------------- + +.. automodule:: api.controllers.bid_controller + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: api.controllers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api.models.rst b/docs/api.models.rst new file mode 100644 index 0000000..8cc8d9b --- /dev/null +++ b/docs/api.models.rst @@ -0,0 +1,37 @@ +api.models package +================== + +Submodules +---------- + +api.models.bid\_model module +---------------------------- + +.. automodule:: api.models.bid_model + :members: + :undoc-members: + :show-inheritance: + +api.models.links\_model module +------------------------------ + +.. automodule:: api.models.links_model + :members: + :undoc-members: + :show-inheritance: + +api.models.status\_enum module +------------------------------ + +.. automodule:: api.models.status_enum + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: api.models + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..477b05e --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,20 @@ +api package +=========== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + api.controllers + api.models + api.schemas + +Module contents +--------------- + +.. automodule:: api + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api.schemas.rst b/docs/api.schemas.rst new file mode 100644 index 0000000..1a82e73 --- /dev/null +++ b/docs/api.schemas.rst @@ -0,0 +1,53 @@ +api.schemas package +=================== + +Submodules +---------- + +api.schemas.bid\_id\_schema module +---------------------------------- + +.. automodule:: api.schemas.bid_id_schema + :members: + :undoc-members: + :show-inheritance: + +api.schemas.bid\_schema module +------------------------------ + +.. automodule:: api.schemas.bid_schema + :members: + :undoc-members: + :show-inheritance: + +api.schemas.feedback\_schema module +----------------------------------- + +.. automodule:: api.schemas.feedback_schema + :members: + :undoc-members: + :show-inheritance: + +api.schemas.links\_schema module +-------------------------------- + +.. automodule:: api.schemas.links_schema + :members: + :undoc-members: + :show-inheritance: + +api.schemas.phase\_schema module +-------------------------------- + +.. automodule:: api.schemas.phase_schema + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: api.schemas + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/app.rst b/docs/app.rst new file mode 100644 index 0000000..ceb7f40 --- /dev/null +++ b/docs/app.rst @@ -0,0 +1,7 @@ +app module +========== + +.. automodule:: app + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..5c4e112 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,31 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) + +project = "BidsApi Documentation" +copyright = "2023, Julio - Pira" +author = "Julio - Pira" +release = "0.4.0" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx.ext.autodoc"] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] diff --git a/docs/dbconfig.rst b/docs/dbconfig.rst new file mode 100644 index 0000000..3d8522b --- /dev/null +++ b/docs/dbconfig.rst @@ -0,0 +1,21 @@ +dbconfig package +================ + +Submodules +---------- + +dbconfig.mongo\_setup module +---------------------------- + +.. automodule:: dbconfig.mongo_setup + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: dbconfig + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/helpers.rst b/docs/helpers.rst new file mode 100644 index 0000000..2748ee6 --- /dev/null +++ b/docs/helpers.rst @@ -0,0 +1,21 @@ +helpers package +=============== + +Submodules +---------- + +helpers.helpers module +---------------------- + +.. automodule:: helpers.helpers + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: helpers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..73b5387 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,20 @@ +.. BidsApi Documentation documentation master file, created by + sphinx-quickstart on Mon Jul 24 19:35:54 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to BidsApi Documentation's documentation! +================================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..45814d0 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,12 @@ +tdse-accessForce-bids-api +========================= + +.. toctree:: + :maxdepth: 4 + + app + dbconfig + helpers + models + controllers + api diff --git a/requirements.txt b/requirements.txt index 42a3674..838fee4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,6 @@ black flake8 pylint coverage -pytest \ No newline at end of file +pytest +sphinx +sphinx-rtd-theme \ No newline at end of file From cc462965be8b9f908a13099b4c84a01a6207a61c Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 25 Jul 2023 09:37:02 +0100 Subject: [PATCH 110/208] feat: start and stop mongo services with makefile --- Makefile | 8 ++++++++ README.md | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 517ae87..a649999 100644 --- a/Makefile +++ b/Makefile @@ -71,6 +71,14 @@ lint: $(PYTHON) -m flake8 $(PYTHON) -m pylint **/*.py **/**/*.py *.py +mongostart: + @echo "Starting MongoDB..." + brew services start mongodb-community@6.0 + +mongostop: + @echo "Stopping MongoDB..." + brew services stop mongodb-community@6.0 + run: build $(PYTHON) app.py diff --git a/README.md b/README.md index 9a60a72..066bddd 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ This API stores and serves information about Methods bids for client tenders. 3. To run MongoDB (i.e. the mongod process) as a macOS service, run: ```bash - brew services start mongodb-community@6.0 + make mongostart ``` 4. To verify that MongoDB is running, run: @@ -107,7 +107,7 @@ This API stores and serves information about Methods bids for client tenders. 5. Run the following command to stop the MongoDB instance, as needed: ```bash - brew services stop mongodb-community@6.0 + make mongostop ``` 6. To begin using MongoDB, connect the MongoDB shell (mongosh) to the running instance. From a new terminal, issue the following: From 19b089a2560cfb3c621621286e3914da3d16f9ad Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 25 Jul 2023 10:21:41 +0100 Subject: [PATCH 111/208] chore: C0114 connection string --- .pylintrc | 2 +- dbconfig/mongo_setup.py | 4 ++++ tests/test_update_bid_status.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index b0aba9d..c3a2132 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,4 +4,4 @@ max-line-length=120 [MESSAGES CONTROL] -disable=C0103,W0613,R0801,C0116,C0114,W0718,R0903,R0913,R0902 \ No newline at end of file +disable=C0103,W0613,R0801,C0116,W0718,R0903,R0913,R0902,W0622 \ No newline at end of file diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index d972f0f..9c9f08e 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -1,3 +1,7 @@ +""" +This file contains the configuration for the MongoDB database. +""" + import os from pymongo import MongoClient from dotenv import load_dotenv diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index b8aec2f..5de301f 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -27,7 +27,7 @@ def test_update_bid_status_success(mock_db, test_client): update = {"status": "completed"} response = test_client.put(f"api/bids/{bid_id}/status", json=update) mock_db["bids"].find_one.assert_called_once_with({"_id": bid_id}) - # mock_db["bids"].replace_one.assert_called_once() + mock_db["bids"].replace_one.assert_called_once() assert response.status_code == 200 From b04ef2ebdb40be438df389d8611d69ce5357a6cc Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 26 Jul 2023 12:26:41 +0100 Subject: [PATCH 112/208] feat: implemented API key authorization for write endpoints --- api/controllers/bid_controller.py | 5 +++++ helpers/helpers.py | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index e9f0591..774748d 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -14,6 +14,7 @@ validate_bid_update, validate_status_update, prepend_host_to_links, + require_api_key, ) bid = Blueprint("bid", __name__) @@ -33,6 +34,7 @@ def get_bids(): @bid.route("/bids", methods=["POST"]) +@require_api_key def post_bid(): try: # Process input and create data model @@ -72,6 +74,7 @@ def get_bid_by_id(bid_id): @bid.route("/bids/", methods=["PUT"]) +@require_api_key def update_bid_by_id(bid_id): try: bid_id = validate_bid_id_path(bid_id) @@ -99,6 +102,7 @@ def update_bid_by_id(bid_id): @bid.route("/bids/", methods=["DELETE"]) +@require_api_key def change_status_to_deleted(bid_id): try: bid_id = validate_bid_id_path(bid_id) @@ -123,6 +127,7 @@ def change_status_to_deleted(bid_id): @bid.route("/bids//status", methods=["PUT"]) +@require_api_key def update_bid_status(bid_id): try: bid_id = validate_bid_id_path(bid_id) diff --git a/helpers/helpers.py b/helpers/helpers.py index b102bec..c817fda 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,11 +1,17 @@ +import os import uuid +from dotenv import load_dotenv from datetime import datetime -from flask import jsonify +from flask import jsonify, request +from functools import wraps from werkzeug.exceptions import UnprocessableEntity from api.schemas.bid_schema import BidSchema from api.schemas.bid_id_schema import BidIdSchema +load_dotenv() + + def showInternalServerError(): return jsonify({"Error": "Could not connect to database"}) @@ -75,3 +81,16 @@ def prepend_host_to_links(resource, hostname): for key in resource["links"]: resource["links"][key] = f'{host}{resource["links"][key]}' return resource + + +def require_api_key(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + try: + api_key = request.headers.get("Authorization") + assert api_key == os.getenv("API_KEY") + except AssertionError: + return jsonify({"Error": "Unauthorized"}), 401 + return fn(*args, **kwargs) + + return wrapper From 93b415c3d421eae4ccf3cf3f83b1ca2d58dcec90 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 26 Jul 2023 14:21:30 +0100 Subject: [PATCH 113/208] test: updated tests --- helpers/helpers.py | 2 +- request_examples/all_fields.http | 1 + tests/test_delete_bid.py | 34 ++++++++++++++++++--- tests/test_phase_schema.py | 8 +++-- tests/test_post_bid.py | 36 ++++++++++++++++++++-- tests/test_update_bid_by_id.py | 50 ++++++++++++++++++++++++++----- tests/test_update_bid_status.py | 51 ++++++++++++++++++++++++++++---- 7 files changed, 160 insertions(+), 22 deletions(-) diff --git a/helpers/helpers.py b/helpers/helpers.py index c817fda..7e383a7 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -87,7 +87,7 @@ def require_api_key(fn): @wraps(fn) def wrapper(*args, **kwargs): try: - api_key = request.headers.get("Authorization") + api_key = request.headers.get("X-API-Key") assert api_key == os.getenv("API_KEY") except AssertionError: return jsonify({"Error": "Unauthorized"}), 401 diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index aa610a7..d5fe43a 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -1,5 +1,6 @@ POST http://localhost:8080/api/bids HTTP/1.1 Content-Type: application/json +X-API-Key: PASSWORD { "tender": "Business Intelligence and Data Warehousing", diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index 3e2798f..46838c8 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -8,7 +8,10 @@ def test_delete_bid_success(mock_db, test_client): "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "deleted", } - response = test_client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", + headers={"X-API-Key": "PASSWORD"}, + ) assert response.status_code == 204 assert response.content_length is None @@ -17,7 +20,10 @@ def test_delete_bid_success(mock_db, test_client): @patch("api.controllers.bid_controller.db") def test_delete_bid_connection_error(mock_db, test_client): mock_db["bids"].find_one_and_update.side_effect = Exception - response = test_client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", + headers={"X-API-Key": "PASSWORD"}, + ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -25,7 +31,9 @@ def test_delete_bid_connection_error(mock_db, test_client): # Case 3: Validation error @patch("api.controllers.bid_controller.db") def test_delete_bid_validation_error(mock_db, test_client): - response = test_client.delete("/api/bids/invalid_bid_id") + response = test_client.delete( + "/api/bids/invalid_bid_id", headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 400 assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} @@ -35,8 +43,26 @@ def test_delete_bid_validation_error(mock_db, test_client): def test_delete_bid_not_found(mock_db, test_client): mock_db["bids"].find_one_and_update.return_value = None - response = test_client.delete("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", + headers={"X-API-Key": "PASSWORD"}, + ) mock_db["bids"].find_one_and_update.assert_called_once() assert response.status_code == 404 assert response.get_json() == {"Error": "Resource not found"} + + +# Case 5: Unauthorized - invalid API key +@patch("api.controllers.bid_controller.db") +def test_delete_bid_unauthorized(mock_db, test_client): + mock_db["bids"].find_one_and_update.return_value = { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "status": "deleted", + } + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", + headers={"X-API-Key": "INVALID"}, + ) + assert response.status_code == 401 + assert response.get_json() == {"Error": "Unauthorized"} diff --git a/tests/test_phase_schema.py b/tests/test_phase_schema.py index ee85177..f8bc8b5 100644 --- a/tests/test_phase_schema.py +++ b/tests/test_phase_schema.py @@ -11,7 +11,9 @@ def test_score_is_mandatory(mock_db, test_client): "success": [{"phase": 1, "has_score": True, "out_of": 36}], } - response = test_client.post("api/bids", json=data) + response = test_client.post( + "api/bids", json=data, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 400 assert ( response.get_json()["Error"] @@ -29,7 +31,9 @@ def test_out_of_is_mandatory(mock_db, test_client): "failed": {"phase": 2, "has_score": True, "score": 20}, } - response = test_client.post("api/bids", json=data) + response = test_client.post( + "api/bids", json=data, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 400 assert ( response.get_json()["Error"] diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 020750b..3d59953 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -21,7 +21,9 @@ def test_post_is_successful(mock_db, test_client): # Mock the behavior of db mock_db["bids"].insert_one.return_value = data - response = test_client.post("api/bids", json=data) + response = test_client.post( + "api/bids", json=data, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 201 assert "_id" in response.get_json() and response.get_json()["_id"] is not None assert ( @@ -48,7 +50,9 @@ def test_post_is_successful(mock_db, test_client): def test_field_missing(mock_db, test_client): data = {"client": "Sample Client", "bid_date": "2023-06-20"} - response = test_client.post("api/bids", json=data) + response = test_client.post( + "api/bids", json=data, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 400 assert response.get_json() == { "Error": "{'tender': {'message': 'Missing mandatory field'}}" @@ -73,7 +77,33 @@ def test_post_bid_connection_error(mock_db, test_client): } # Mock the behavior of db mock_db["bids"].insert_one.side_effect = Exception - response = test_client.post("/api/bids", json=data) + response = test_client.post( + "/api/bids", json=data, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} + + +# Case 4: Unauthorized - invalid API key +@patch("api.controllers.bid_controller.db") +def test_post_bid_unauthorized(mock_db, test_client): + 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": 2, "has_score": True, "score": 20, "out_of": 36}, + } + + # Mock the behavior of db + mock_db["bids"].insert_one.return_value = data + response = test_client.post("api/bids", json=data, headers={"X-API-Key": "INVALID"}) + assert response.status_code == 401 + assert response.get_json() == {"Error": "Unauthorized"} diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index ad824fb..caa47ef 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -16,7 +16,9 @@ def test_update_bid_by_id_success(mock_db, test_client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "UPDATED TENDER"} - response = test_client.put(f"api/bids/{bid_id}", json=update) + response = test_client.put( + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + ) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} ) @@ -47,7 +49,9 @@ def test_input_validation(mock_db, test_client): } bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"tender": 42} - response = test_client.put(f"api/bids/{bid_id}", json=update) + response = test_client.put( + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 400 assert response.get_json()["Error"] == "{'tender': ['Not a valid string.']}" @@ -58,7 +62,9 @@ def test_bid_not_found(mock_db, test_client): mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} - response = test_client.put(f"api/bids/{bid_id}", json=update) + response = test_client.put( + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 404 assert response.get_json()["Error"] == "Resource not found" @@ -68,7 +74,9 @@ def test_bid_not_found(mock_db, test_client): def test_cannot_update_status(mock_db, test_client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "deleted"} - response = test_client.put(f"api/bids/{bid_id}", json=update) + response = test_client.put( + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 422 assert response.get_json()["Error"] == "Cannot update status" @@ -88,7 +96,9 @@ def test_update_by_id_find_error(mock_db, test_client): mock_db["bids"].find_one.side_effect = Exception bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} - response = test_client.put(f"api/bids/{bid_id}", json=update) + response = test_client.put( + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -116,7 +126,9 @@ def test_update_failed(mock_db, test_client): } bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}} - response = test_client.put(f"api/bids/{bid_id}", json=update) + response = test_client.put( + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + ) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} ) @@ -152,9 +164,33 @@ def test_update_success(mock_db, test_client): {"phase": 2, "has_score": True, "score": 20, "out_of": 36}, ] } - response = test_client.put(f"api/bids/{bid_id}", json=update) + response = test_client.put( + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + ) 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 8: Unauthorized - invalid API key +@patch("api.controllers.bid_controller.db") +def test_update_bid_by_id_unauthorized(mock_db, test_client): + 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, + } + + bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" + update = {"tender": "UPDATED TENDER"} + response = test_client.put( + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "INVALID"} + ) + assert response.status_code == 401 + assert response.get_json() == {"Error": "Unauthorized"} diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index 5de301f..5228a9b 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -25,7 +25,9 @@ def test_update_bid_status_success(mock_db, test_client): bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"status": "completed"} - response = test_client.put(f"api/bids/{bid_id}/status", json=update) + response = test_client.put( + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + ) 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 @@ -54,7 +56,9 @@ def test_invalid_status(mock_db, test_client): } bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "invalid"} - response = test_client.put(f"api/bids/{bid_id}/status", json=update) + response = test_client.put( + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 400 assert ( response.get_json()["Error"] @@ -67,7 +71,9 @@ def test_invalid_status(mock_db, test_client): def test_empty_request(mock_db, test_client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {} - response = test_client.put(f"api/bids/{bid_id}/status", json=update) + response = test_client.put( + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 422 assert response.get_json()["Error"] == "Request must not be empty" @@ -78,7 +84,9 @@ def test_bid_not_found(mock_db, test_client): mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} - response = test_client.put(f"api/bids/{bid_id}/status", json=update) + response = test_client.put( + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 404 assert response.get_json()["Error"] == "Resource not found" @@ -89,6 +97,39 @@ def test_update_status_find_error(mock_db, test_client): mock_db["bids"].find_one.side_effect = Exception bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} - response = test_client.put(f"api/bids/{bid_id}/status", json=update) + response = test_client.put( + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} + + +# Case 6: Unauthorized - invalid API key +@patch("api.controllers.bid_controller.db") +def test_update_bid_status_unauthorized(mock_db, test_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 = "4141fac8-8879-4169-a46d-2effb1f515f6" + update = {"status": "completed"} + response = test_client.put( + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "INVALID"} + ) + assert response.status_code == 401 + assert response.get_json() == {"Error": "Unauthorized"} From 3e055fbe26f41d2f16fc7642cef1765971f99a99 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 26 Jul 2023 16:11:03 +0100 Subject: [PATCH 114/208] refactor: added session scope to pytest fixture to improve speed; added api_key to conftest to improve maintainability --- tests/conftest.py | 11 ++++++++++- tests/test_delete_bid.py | 18 +++++++++--------- tests/test_get_bid_by_id.py | 8 ++++---- tests/test_get_bids.py | 6 +++--- tests/test_phase_schema.py | 12 ++++-------- tests/test_post_bid.py | 20 +++++++------------- tests/test_update_bid_by_id.py | 30 +++++++++++++++--------------- tests/test_update_bid_status.py | 22 +++++++++++----------- 8 files changed, 63 insertions(+), 64 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 25e8539..31d5d0e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,20 @@ +import os import pytest +from dotenv import load_dotenv from flask import Flask from api.controllers.bid_controller import bid -@pytest.fixture +@pytest.fixture(scope="session") def test_client(): app = Flask(__name__) app.register_blueprint(bid, url_prefix="/api") with app.test_client() as client: yield client + + +@pytest.fixture(scope="session") +def api_key(): + load_dotenv() + api_key = os.getenv("API_KEY") + return api_key diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index 46838c8..31736f4 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -3,14 +3,14 @@ # Case 1: Successful delete a bid by changing status to deleted @patch("api.controllers.bid_controller.db") -def test_delete_bid_success(mock_db, test_client): +def test_delete_bid_success(mock_db, test_client, api_key): mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "deleted", } response = test_client.delete( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"X-API-Key": "PASSWORD"}, + headers={"X-API-Key": api_key}, ) assert response.status_code == 204 assert response.content_length is None @@ -18,11 +18,11 @@ def test_delete_bid_success(mock_db, test_client): # Case 2: Failed to call database @patch("api.controllers.bid_controller.db") -def test_delete_bid_connection_error(mock_db, test_client): +def test_delete_bid_connection_error(mock_db, test_client, api_key): mock_db["bids"].find_one_and_update.side_effect = Exception response = test_client.delete( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"X-API-Key": "PASSWORD"}, + headers={"X-API-Key": api_key}, ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -30,9 +30,9 @@ def test_delete_bid_connection_error(mock_db, test_client): # Case 3: Validation error @patch("api.controllers.bid_controller.db") -def test_delete_bid_validation_error(mock_db, test_client): +def test_delete_bid_validation_error(mock_db, test_client, api_key): response = test_client.delete( - "/api/bids/invalid_bid_id", headers={"X-API-Key": "PASSWORD"} + "/api/bids/invalid_bid_id", headers={"X-API-Key": api_key} ) assert response.status_code == 400 assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} @@ -40,12 +40,12 @@ def test_delete_bid_validation_error(mock_db, test_client): # Case 4: Bid not found @patch("api.controllers.bid_controller.db") -def test_delete_bid_not_found(mock_db, test_client): +def test_delete_bid_not_found(mock_db, test_client, api_key): mock_db["bids"].find_one_and_update.return_value = None response = test_client.delete( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"X-API-Key": "PASSWORD"}, + headers={"X-API-Key": api_key}, ) mock_db["bids"].find_one_and_update.assert_called_once() @@ -55,7 +55,7 @@ def test_delete_bid_not_found(mock_db, test_client): # Case 5: Unauthorized - invalid API key @patch("api.controllers.bid_controller.db") -def test_delete_bid_unauthorized(mock_db, test_client): +def test_delete_bid_unauthorized(mock_db, test_client, api_key): mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "deleted", diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index c4e68aa..b1f7dd2 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -3,7 +3,7 @@ # Case 1: Successful get_bid_by_id @patch("api.controllers.bid_controller.db") -def test_get_bid_by_id_success(mock_db, test_client): +def test_get_bid_by_id_success(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "alias": "ONS", @@ -46,7 +46,7 @@ def test_get_bid_by_id_success(mock_db, test_client): # Case 2: Connection error @patch("api.controllers.bid_controller.db", side_effect=Exception) -def test_get_bid_by_id_connection_error(mock_db, test_client): +def test_get_bid_by_id_connection_error(mock_db, test_client, api_key): mock_db["bids"].find_one.side_effect = Exception response = test_client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") assert response.status_code == 500 @@ -55,7 +55,7 @@ def test_get_bid_by_id_connection_error(mock_db, test_client): # Case 3: Bid not found @patch("api.controllers.bid_controller.db") -def test_get_bid_by_id_not_found(mock_db, test_client): +def test_get_bid_by_id_not_found(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = None response = test_client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") @@ -69,7 +69,7 @@ def test_get_bid_by_id_not_found(mock_db, test_client): # Case 4: Validation error @patch("api.controllers.bid_controller.db") -def test_get_bid_by_id_validation_error(mock_db, test_client): +def test_get_bid_by_id_validation_error(mock_db, test_client, api_key): response = test_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 414fe4b..f50aac1 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.db") -def test_get_bids_success(mock_db, test_client): +def test_get_bids_success(mock_db, test_client, api_key): mock_db["bids"].find.return_value = [] response = test_client.get("/api/bids") @@ -14,7 +14,7 @@ def test_get_bids_success(mock_db, test_client): # Case 2: Links prepended with hostname @patch("api.controllers.bid_controller.db") -def test_links_with_host(mock_db, test_client): +def test_links_with_host(mock_db, test_client, api_key): mock_db["bids"].find.return_value = [ { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", @@ -51,7 +51,7 @@ def test_links_with_host(mock_db, test_client): # Case 3: Connection error @patch("api.controllers.bid_controller.db") -def test_get_bids_connection_error(mock_db, test_client): +def test_get_bids_connection_error(mock_db, test_client, api_key): mock_db["bids"].find.side_effect = Exception response = test_client.get("/api/bids") assert response.status_code == 500 diff --git a/tests/test_phase_schema.py b/tests/test_phase_schema.py index f8bc8b5..b6cec4b 100644 --- a/tests/test_phase_schema.py +++ b/tests/test_phase_schema.py @@ -3,7 +3,7 @@ # Case 1: score is mandatory when has_score is set to True @patch("api.controllers.bid_controller.db") -def test_score_is_mandatory(mock_db, test_client): +def test_score_is_mandatory(mock_db, test_client, api_key): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -11,9 +11,7 @@ def test_score_is_mandatory(mock_db, test_client): "success": [{"phase": 1, "has_score": True, "out_of": 36}], } - response = test_client.post( - "api/bids", json=data, headers={"X-API-Key": "PASSWORD"} - ) + response = test_client.post("api/bids", json=data, headers={"X-API-Key": api_key}) assert response.status_code == 400 assert ( response.get_json()["Error"] @@ -23,7 +21,7 @@ def test_score_is_mandatory(mock_db, test_client): # Case 2: out_of is mandatory when has_score is set to True @patch("api.controllers.bid_controller.db") -def test_out_of_is_mandatory(mock_db, test_client): +def test_out_of_is_mandatory(mock_db, test_client, api_key): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -31,9 +29,7 @@ def test_out_of_is_mandatory(mock_db, test_client): "failed": {"phase": 2, "has_score": True, "score": 20}, } - response = test_client.post( - "api/bids", json=data, headers={"X-API-Key": "PASSWORD"} - ) + response = test_client.post("api/bids", json=data, headers={"X-API-Key": api_key}) assert response.status_code == 400 assert ( response.get_json()["Error"] diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 3d59953..b39295f 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -3,7 +3,7 @@ # Case 1: Successful post @patch("api.controllers.bid_controller.db") -def test_post_is_successful(mock_db, test_client): +def test_post_is_successful(mock_db, test_client, api_key): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -21,9 +21,7 @@ def test_post_is_successful(mock_db, test_client): # Mock the behavior of db mock_db["bids"].insert_one.return_value = data - response = test_client.post( - "api/bids", json=data, headers={"X-API-Key": "PASSWORD"} - ) + response = test_client.post("api/bids", json=data, headers={"X-API-Key": api_key}) assert response.status_code == 201 assert "_id" in response.get_json() and response.get_json()["_id"] is not None assert ( @@ -47,12 +45,10 @@ def test_post_is_successful(mock_db, test_client): # Case 2: Missing mandatory fields @patch("api.controllers.bid_controller.db") -def test_field_missing(mock_db, test_client): +def test_field_missing(mock_db, test_client, api_key): data = {"client": "Sample Client", "bid_date": "2023-06-20"} - response = test_client.post( - "api/bids", json=data, headers={"X-API-Key": "PASSWORD"} - ) + response = test_client.post("api/bids", json=data, headers={"X-API-Key": api_key}) assert response.status_code == 400 assert response.get_json() == { "Error": "{'tender': {'message': 'Missing mandatory field'}}" @@ -61,7 +57,7 @@ def test_field_missing(mock_db, test_client): # Case 3: Connection error @patch("api.controllers.bid_controller.db") -def test_post_bid_connection_error(mock_db, test_client): +def test_post_bid_connection_error(mock_db, test_client, api_key): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -77,9 +73,7 @@ def test_post_bid_connection_error(mock_db, test_client): } # Mock the behavior of db mock_db["bids"].insert_one.side_effect = Exception - response = test_client.post( - "/api/bids", json=data, headers={"X-API-Key": "PASSWORD"} - ) + response = test_client.post("/api/bids", json=data, headers={"X-API-Key": api_key}) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -87,7 +81,7 @@ def test_post_bid_connection_error(mock_db, test_client): # Case 4: Unauthorized - invalid API key @patch("api.controllers.bid_controller.db") -def test_post_bid_unauthorized(mock_db, test_client): +def test_post_bid_unauthorized(mock_db, test_client, api_key): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index caa47ef..a3a3d36 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -3,7 +3,7 @@ # Case 1: Successful update @patch("api.controllers.bid_controller.db") -def test_update_bid_by_id_success(mock_db, test_client): +def test_update_bid_by_id_success(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", @@ -17,7 +17,7 @@ def test_update_bid_by_id_success(mock_db, test_client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "UPDATED TENDER"} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} ) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} @@ -28,7 +28,7 @@ def test_update_bid_by_id_success(mock_db, test_client): # Case 2: Invalid user input @patch("api.controllers.bid_controller.db") -def test_input_validation(mock_db, test_client): +def test_input_validation(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -50,7 +50,7 @@ def test_input_validation(mock_db, test_client): bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"tender": 42} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} ) assert response.status_code == 400 assert response.get_json()["Error"] == "{'tender': ['Not a valid string.']}" @@ -58,12 +58,12 @@ def test_input_validation(mock_db, test_client): # Case 3: Bid not found @patch("api.controllers.bid_controller.db") -def test_bid_not_found(mock_db, test_client): +def test_bid_not_found(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} ) assert response.status_code == 404 assert response.get_json()["Error"] == "Resource not found" @@ -71,11 +71,11 @@ def test_bid_not_found(mock_db, test_client): # Case 4: Cannot update status @patch("api.controllers.bid_controller.db") -def test_cannot_update_status(mock_db, test_client): +def test_cannot_update_status(mock_db, test_client, api_key): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "deleted"} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} ) assert response.status_code == 422 assert response.get_json()["Error"] == "Cannot update status" @@ -83,7 +83,7 @@ def test_cannot_update_status(mock_db, test_client): # Case 5: Failed to call database @patch("api.controllers.bid_controller.db") -def test_update_by_id_find_error(mock_db, test_client): +def test_update_by_id_find_error(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", @@ -97,7 +97,7 @@ def test_update_by_id_find_error(mock_db, test_client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -105,7 +105,7 @@ def test_update_by_id_find_error(mock_db, test_client): # Case 6: Update failed field @patch("api.controllers.bid_controller.db") -def test_update_failed(mock_db, test_client): +def test_update_failed(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -127,7 +127,7 @@ def test_update_failed(mock_db, test_client): bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} ) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} @@ -138,7 +138,7 @@ def test_update_failed(mock_db, test_client): # Case 7: Update success field @patch("api.controllers.bid_controller.db") -def test_update_success(mock_db, test_client): +def test_update_success(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -165,7 +165,7 @@ def test_update_success(mock_db, test_client): ] } response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} ) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} @@ -176,7 +176,7 @@ def test_update_success(mock_db, test_client): # Case 8: Unauthorized - invalid API key @patch("api.controllers.bid_controller.db") -def test_update_bid_by_id_unauthorized(mock_db, test_client): +def test_update_bid_by_id_unauthorized(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index 5228a9b..13e4d77 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -3,7 +3,7 @@ # Case 1: Successful update @patch("api.controllers.bid_controller.db") -def test_update_bid_status_success(mock_db, test_client): +def test_update_bid_status_success(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -26,7 +26,7 @@ def test_update_bid_status_success(mock_db, test_client): bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"status": "completed"} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} ) mock_db["bids"].find_one.assert_called_once_with({"_id": bid_id}) mock_db["bids"].replace_one.assert_called_once() @@ -35,7 +35,7 @@ def test_update_bid_status_success(mock_db, test_client): # Case 2: Invalid status @patch("api.controllers.bid_controller.db") -def test_invalid_status(mock_db, test_client): +def test_invalid_status(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -57,7 +57,7 @@ def test_invalid_status(mock_db, test_client): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "invalid"} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} ) assert response.status_code == 400 assert ( @@ -68,11 +68,11 @@ def test_invalid_status(mock_db, test_client): # Case 3: Empty request body @patch("api.controllers.bid_controller.db") -def test_empty_request(mock_db, test_client): +def test_empty_request(mock_db, test_client, api_key): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} ) assert response.status_code == 422 assert response.get_json()["Error"] == "Request must not be empty" @@ -80,12 +80,12 @@ def test_empty_request(mock_db, test_client): # Case 4: Bid not found @patch("api.controllers.bid_controller.db") -def test_bid_not_found(mock_db, test_client): +def test_bid_not_found(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} ) assert response.status_code == 404 assert response.get_json()["Error"] == "Resource not found" @@ -93,12 +93,12 @@ def test_bid_not_found(mock_db, test_client): # Case 5: Failed to call database @patch("api.controllers.bid_controller.db") -def test_update_status_find_error(mock_db, test_client): +def test_update_status_find_error(mock_db, test_client, api_key): mock_db["bids"].find_one.side_effect = Exception bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "PASSWORD"} + f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -106,7 +106,7 @@ def test_update_status_find_error(mock_db, test_client): # Case 6: Unauthorized - invalid API key @patch("api.controllers.bid_controller.db") -def test_update_bid_status_unauthorized(mock_db, test_client): +def test_update_bid_status_unauthorized(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", From 5ce1c33ea374b90d3c69993a7b5462e5f234dee3 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 26 Jul 2023 18:49:45 +0100 Subject: [PATCH 115/208] chore: added makefile target to run auth api app --- .DS_Store | Bin 0 -> 8196 bytes Makefile | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7c5b113de3ecd15ffa2cc580ea140857f0c46f76 GIT binary patch literal 8196 zcmeHMJ!n)x5T4Cl%mocZ1PzhELlA<0B$8-Ak(Y2HScH%uq|)SmOagbgGk+*HCs>I^ zYAeCc%Em@e(!@#-!Abun&?yX{W^>CV z)P1A%y=TBP@Lw{(`60l=GPJU0q)r{^^c4UyjbYvJyPtnR23Cev){KM(O&M26b z|0H$gN;aD-RdN{1ynZnAac5$8($yE*>Z{A*+d#MjSL&(9rDfWroD-c=g}8;paAozy ziHm{85Sgc3X(CCsa`2dB(+50NxQ+C{K%cmZ|7sJHW}NjkUx{XNS){Fwj|z6xP98yN__VQXFne@_=LaYu(gGZ-#h<|Z+4G0_EXFwu_RkbJhldp=FrFY}*aNHg~IaILSlIO{B50!$UG8B zvXzS`WIX*qQKVbQHTCmdq}zB$%0O7+F_)jdmwWM%n2&s&y>{5VL%)qBSMeu)?z$fH zx_b1PTf-YyMb?U};^<)9LSi2IFkR2(k$EJ-WGfd>$awbY?70XmE0lLRy>K_GmsnoS zPrtWy#hF7QNwR5yqq~vGT~5?@-dUlQ7>36u$8ElLESkBXwS^PEOXox8qLW+7fE(cZ z|79Ex{~e%{cgth*4E!AiR60ACoxK`S2I$(pwriJNPQiLZ2Vz} u-vLb7C$zF=Bu3Ew`G){s|JP-`ORns)>Lyz`b Date: Wed, 26 Jul 2023 18:56:14 +0100 Subject: [PATCH 116/208] chore: removed .ds_store and added to .gitignore --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 7c5b113de3ecd15ffa2cc580ea140857f0c46f76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMJ!n)x5T4Cl%mocZ1PzhELlA<0B$8-Ak(Y2HScH%uq|)SmOagbgGk+*HCs>I^ zYAeCc%Em@e(!@#-!Abun&?yX{W^>CV z)P1A%y=TBP@Lw{(`60l=GPJU0q)r{^^c4UyjbYvJyPtnR23Cev){KM(O&M26b z|0H$gN;aD-RdN{1ynZnAac5$8($yE*>Z{A*+d#MjSL&(9rDfWroD-c=g}8;paAozy ziHm{85Sgc3X(CCsa`2dB(+50NxQ+C{K%cmZ|7sJHW}NjkUx{XNS){Fwj|z6xP98yN__VQXFne@_=LaYu(gGZ-#h<|Z+4G0_EXFwu_RkbJhldp=FrFY}*aNHg~IaILSlIO{B50!$UG8B zvXzS`WIX*qQKVbQHTCmdq}zB$%0O7+F_)jdmwWM%n2&s&y>{5VL%)qBSMeu)?z$fH zx_b1PTf-YyMb?U};^<)9LSi2IFkR2(k$EJ-WGfd>$awbY?70XmE0lLRy>K_GmsnoS zPrtWy#hF7QNwR5yqq~vGT~5?@-dUlQ7>36u$8ElLESkBXwS^PEOXox8qLW+7fE(cZ z|79Ex{~e%{cgth*4E!AiR60ACoxK`S2I$(pwriJNPQiLZ2Vz} u-vLb7C$zF=Bu3Ew`G){s|JP-`ORns)>Lyz`b Date: Thu, 27 Jul 2023 15:41:10 +0100 Subject: [PATCH 117/208] feat: implement jwt verification; added scripts for posting with auth --- api/controllers/bid_controller.py | 7 ++-- helpers/helpers.py | 50 ++++++++++++++++++++++++- request_examples/get_jwt_from_auth.http | 9 +++++ scripts/post_apikey.py | 23 ++++++++++++ scripts/post_jwt.py | 37 ++++++++++++++++++ 5 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 request_examples/get_jwt_from_auth.http create mode 100644 scripts/post_apikey.py create mode 100644 scripts/post_jwt.py diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 774748d..3780682 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -15,6 +15,7 @@ validate_status_update, prepend_host_to_links, require_api_key, + require_jwt, ) bid = Blueprint("bid", __name__) @@ -34,7 +35,7 @@ def get_bids(): @bid.route("/bids", methods=["POST"]) -@require_api_key +@require_jwt def post_bid(): try: # Process input and create data model @@ -46,8 +47,8 @@ def post_bid(): except ValidationError as e: return showValidationError(e), 400 # Return 500 response in case of connection failure - except Exception: - return showInternalServerError(), 500 + except Exception as e: + return str(e), 500 @bid.route("/bids/", methods=["GET"]) diff --git a/helpers/helpers.py b/helpers/helpers.py index 7e383a7..850466e 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,3 +1,5 @@ +import jwt +from jwt.exceptions import InvalidTokenError import os import uuid from dotenv import load_dotenv @@ -9,7 +11,8 @@ from api.schemas.bid_id_schema import BidIdSchema -load_dotenv() +def showForbiddenError(): + return jsonify({"Error": "Forbidden - insufficient permissions"}) def showInternalServerError(): @@ -20,6 +23,10 @@ def showNotFoundError(): return jsonify({"Error": "Resource not found"}) +def showUnauthorizedError(): + return jsonify({"Error": "Unauthorized - invalid token"}) + + def showUnprocessableEntityError(e): return jsonify({"Error": str(e.description)}) @@ -87,10 +94,49 @@ def require_api_key(fn): @wraps(fn) def wrapper(*args, **kwargs): try: + load_dotenv() api_key = request.headers.get("X-API-Key") assert api_key == os.getenv("API_KEY") except AssertionError: - return jsonify({"Error": "Unauthorized"}), 401 + return showUnauthorizedError(), 401 + return fn(*args, **kwargs) + + return wrapper + + +def require_jwt(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + try: + PREFIX = "Bearer " + auth_header = request.headers.get("Authorization") + assert auth_header.startswith(PREFIX) is True + token = auth_header[len(PREFIX) :] + load_dotenv() + key = os.getenv("SECRET") + jwt.decode(token, key, algorithms="HS256") + except (AssertionError, InvalidTokenError): + return showUnauthorizedError(), 401 + return fn(*args, **kwargs) + + return wrapper + + +def require_admin_access(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + try: + PREFIX = "Bearer " + auth_header = request.headers.get("Authorization") + assert auth_header.startswith(PREFIX) is True + token = auth_header[len(PREFIX) :] + load_dotenv() + key = os.getenv("SECRET") + decoded = jwt.decode(token, key, algorithms="HS256") + if decoded["admin"] is False: + return showForbiddenError(), 403 + except (AssertionError, InvalidTokenError): + return showUnauthorizedError(), 401 return fn(*args, **kwargs) return wrapper diff --git a/request_examples/get_jwt_from_auth.http b/request_examples/get_jwt_from_auth.http new file mode 100644 index 0000000..020fd0b --- /dev/null +++ b/request_examples/get_jwt_from_auth.http @@ -0,0 +1,9 @@ +POST http://localhost:5000/authorise/ HTTP/1.1 +Content-Type: application/json + +{ + "username": "Tester McTestface", + "admin": false, + "write": true, + "read": true +} diff --git a/scripts/post_apikey.py b/scripts/post_apikey.py new file mode 100644 index 0000000..5998f07 --- /dev/null +++ b/scripts/post_apikey.py @@ -0,0 +1,23 @@ +import json +import requests + +response = requests.get("http://localhost:5000/authorise/") + +api_key = response.json()["API_KEY"] + +print(api_key) + +data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", +} + +headers = {"Content-Type": "application/json", "X-API-Key": api_key} + +post_response = requests.post( + "http://localhost:8080/api/bids", data=json.dumps(data), headers=headers +) + +print(post_response) diff --git a/scripts/post_jwt.py b/scripts/post_jwt.py new file mode 100644 index 0000000..8436834 --- /dev/null +++ b/scripts/post_jwt.py @@ -0,0 +1,37 @@ +import json +import requests + + +user_info = { + "username": "Tester McTestface", + "admin": False, + "write": True, + "read": True, +} + +response = requests.post( + "http://localhost:5000/authorise/", + data=json.dumps(user_info), + headers={"Content-Type": "application/json"}, +) + +token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRlc3RlciBNY1Rlc3RmYWNlIiwiYWRtaW4iOmZhbHNlLCJ3cml0ZSI6dHJ1ZSwicmVhZCI6dHJ1ZX0.EtqxvNchJ7t0R10JSQZIz7-ShhRdgRllAMgnSya3tOr" + +print(token) + +data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", +} + +fstring = f"Bearer {token}" + +headers = {"Content-Type": "application/json", "Authorization": fstring} + +post_response = requests.post( + "http://localhost:8080/api/bids", data=json.dumps(data), headers=headers +) + +print(post_response) From 15dffa42955a600a93b97a024dc9ab54e451d6cd Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 28 Jul 2023 10:02:05 +0100 Subject: [PATCH 118/208] feat: implemented require_admin wrapper; scripts to demo api interactions for get and post --- api/controllers/bid_controller.py | 9 ++++--- helpers/helpers.py | 27 +++++++++---------- request_examples/all_fields.http | 2 +- request_examples/delete.http | 3 ++- request_examples/get_all.http | 1 + request_examples/get_jwt_from_auth.http | 4 +-- .../{post_apikey.py => get_bids_apikey.py} | 5 +--- scripts/{post_jwt.py => post_bid_jwt.py} | 9 ++----- 8 files changed, 27 insertions(+), 33 deletions(-) rename scripts/{post_apikey.py => get_bids_apikey.py} (78%) rename scripts/{post_jwt.py => post_bid_jwt.py} (68%) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 3780682..fab55c0 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -16,12 +16,14 @@ prepend_host_to_links, require_api_key, require_jwt, + require_admin_access, ) bid = Blueprint("bid", __name__) @bid.route("/bids", methods=["GET"]) +@require_api_key def get_bids(): # Get all bids from database collection try: @@ -52,6 +54,7 @@ def post_bid(): @bid.route("/bids/", methods=["GET"]) +@require_api_key def get_bid_by_id(bid_id): try: bid_id = validate_bid_id_path(bid_id) @@ -75,7 +78,7 @@ def get_bid_by_id(bid_id): @bid.route("/bids/", methods=["PUT"]) -@require_api_key +@require_jwt def update_bid_by_id(bid_id): try: bid_id = validate_bid_id_path(bid_id) @@ -103,7 +106,7 @@ def update_bid_by_id(bid_id): @bid.route("/bids/", methods=["DELETE"]) -@require_api_key +@require_admin_access def change_status_to_deleted(bid_id): try: bid_id = validate_bid_id_path(bid_id) @@ -128,7 +131,7 @@ def change_status_to_deleted(bid_id): @bid.route("/bids//status", methods=["PUT"]) -@require_api_key +@require_admin_access def update_bid_status(bid_id): try: bid_id = validate_bid_id_path(bid_id) diff --git a/helpers/helpers.py b/helpers/helpers.py index 850466e..5600138 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -108,13 +108,7 @@ def require_jwt(fn): @wraps(fn) def wrapper(*args, **kwargs): try: - PREFIX = "Bearer " - auth_header = request.headers.get("Authorization") - assert auth_header.startswith(PREFIX) is True - token = auth_header[len(PREFIX) :] - load_dotenv() - key = os.getenv("SECRET") - jwt.decode(token, key, algorithms="HS256") + validate_token(request=request) except (AssertionError, InvalidTokenError): return showUnauthorizedError(), 401 return fn(*args, **kwargs) @@ -126,13 +120,7 @@ def require_admin_access(fn): @wraps(fn) def wrapper(*args, **kwargs): try: - PREFIX = "Bearer " - auth_header = request.headers.get("Authorization") - assert auth_header.startswith(PREFIX) is True - token = auth_header[len(PREFIX) :] - load_dotenv() - key = os.getenv("SECRET") - decoded = jwt.decode(token, key, algorithms="HS256") + decoded = validate_token(request=request) if decoded["admin"] is False: return showForbiddenError(), 403 except (AssertionError, InvalidTokenError): @@ -140,3 +128,14 @@ def wrapper(*args, **kwargs): return fn(*args, **kwargs) return wrapper + + +def validate_token(request): + PREFIX = "Bearer " + auth_header = request.headers.get("Authorization") + assert auth_header.startswith(PREFIX) is True + token = auth_header[len(PREFIX) :] + load_dotenv() + key = os.getenv("SECRET") + decoded = jwt.decode(token, key, algorithms="HS256") + return decoded diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index d5fe43a..d2de3ea 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -1,6 +1,6 @@ POST http://localhost:8080/api/bids HTTP/1.1 Content-Type: application/json -X-API-Key: PASSWORD +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRlc3RlciBNY1Rlc3RmYWNlIiwiYWRtaW4iOmZhbHNlfQ.Dg7f8LVtALYWvjZH31re5C-Pc6Hp6Ra-U4LAy0ZQQ9M { "tender": "Business Intelligence and Data Warehousing", diff --git a/request_examples/delete.http b/request_examples/delete.http index 8814193..1d84f07 100644 --- a/request_examples/delete.http +++ b/request_examples/delete.http @@ -1 +1,2 @@ -DELETE http://localhost:8080/api/bids/27fc4afb-fcdc-4bf7-9f8f-74f524831da4 HTTP/1.1 +DELETE http://localhost:8080/api/bids/b4846631-9135-4208-8e37-70eba8f77e15 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRlc3RlciBNY1Rlc3RmYWNlIiwiYWRtaW4iOmZhbHNlfQ.Dg7f8LVtALYWvjZH31re5C-Pc6Hp6Ra-U4LAy0ZQQ9M diff --git a/request_examples/get_all.http b/request_examples/get_all.http index 712676d..51e3ef0 100644 --- a/request_examples/get_all.http +++ b/request_examples/get_all.http @@ -1 +1,2 @@ GET http://localhost:8080/api/bids HTTP/1.1 +X-API-Key: THIS_IS_THE_API_KEY \ No newline at end of file diff --git a/request_examples/get_jwt_from_auth.http b/request_examples/get_jwt_from_auth.http index 020fd0b..559d2d3 100644 --- a/request_examples/get_jwt_from_auth.http +++ b/request_examples/get_jwt_from_auth.http @@ -3,7 +3,5 @@ Content-Type: application/json { "username": "Tester McTestface", - "admin": false, - "write": true, - "read": true + "admin": false } diff --git a/scripts/post_apikey.py b/scripts/get_bids_apikey.py similarity index 78% rename from scripts/post_apikey.py rename to scripts/get_bids_apikey.py index 5998f07..7d42aca 100644 --- a/scripts/post_apikey.py +++ b/scripts/get_bids_apikey.py @@ -1,4 +1,3 @@ -import json import requests response = requests.get("http://localhost:5000/authorise/") @@ -16,8 +15,6 @@ headers = {"Content-Type": "application/json", "X-API-Key": api_key} -post_response = requests.post( - "http://localhost:8080/api/bids", data=json.dumps(data), headers=headers -) +post_response = requests.get("http://localhost:8080/api/bids", headers=headers) print(post_response) diff --git a/scripts/post_jwt.py b/scripts/post_bid_jwt.py similarity index 68% rename from scripts/post_jwt.py rename to scripts/post_bid_jwt.py index 8436834..103dd12 100644 --- a/scripts/post_jwt.py +++ b/scripts/post_bid_jwt.py @@ -2,12 +2,7 @@ import requests -user_info = { - "username": "Tester McTestface", - "admin": False, - "write": True, - "read": True, -} +user_info = {"username": "Tester McTestface", "admin": False} response = requests.post( "http://localhost:5000/authorise/", @@ -15,7 +10,7 @@ headers={"Content-Type": "application/json"}, ) -token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRlc3RlciBNY1Rlc3RmYWNlIiwiYWRtaW4iOmZhbHNlLCJ3cml0ZSI6dHJ1ZSwicmVhZCI6dHJ1ZX0.EtqxvNchJ7t0R10JSQZIz7-ShhRdgRllAMgnSya3tOr" +token = response.json()["jwt"] print(token) From bf7a045441d540a65a5dd4c1ac66cae50bfc361f Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 28 Jul 2023 11:08:52 +0100 Subject: [PATCH 119/208] test: updated tests with new permissions required --- api/controllers/bid_controller.py | 4 +-- helpers/helpers.py | 4 +-- tests/conftest.py | 21 ++++++++++++- tests/test_delete_bid.py | 22 +++++++------- tests/test_get_bid_by_id.py | 17 ++++++++--- tests/test_get_bids.py | 12 ++++++-- tests/test_phase_schema.py | 12 +++++--- tests/test_post_bid.py | 26 ++++++++++------ tests/test_update_bid_by_id.py | 50 ++++++++++++++++++++----------- tests/test_update_bid_status.py | 38 +++++++++++++++-------- 10 files changed, 140 insertions(+), 66 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index fab55c0..e700c76 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -49,8 +49,8 @@ def post_bid(): except ValidationError as e: return showValidationError(e), 400 # Return 500 response in case of connection failure - except Exception as e: - return str(e), 500 + except Exception: + return showInternalServerError(), 500 @bid.route("/bids/", methods=["GET"]) diff --git a/helpers/helpers.py b/helpers/helpers.py index 5600138..ad38d4d 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -12,7 +12,7 @@ def showForbiddenError(): - return jsonify({"Error": "Forbidden - insufficient permissions"}) + return jsonify({"Error": "Forbidden"}) def showInternalServerError(): @@ -24,7 +24,7 @@ def showNotFoundError(): def showUnauthorizedError(): - return jsonify({"Error": "Unauthorized - invalid token"}) + return jsonify({"Error": "Unauthorized"}) def showUnprocessableEntityError(e): diff --git a/tests/conftest.py b/tests/conftest.py index 31d5d0e..21445b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ -import os +import jwt import pytest +import os from dotenv import load_dotenv from flask import Flask from api.controllers.bid_controller import bid @@ -18,3 +19,21 @@ def api_key(): load_dotenv() api_key = os.getenv("API_KEY") return api_key + + +@pytest.fixture(scope="session") +def basic_jwt(): + payload = {"username": "User McTestface", "admin": False} + load_dotenv() + key = os.getenv("SECRET") + token = jwt.encode(payload=payload, key=key) + return token + + +@pytest.fixture(scope="session") +def admin_jwt(): + payload = {"username": "Admin McTestface", "admin": True} + load_dotenv() + key = os.getenv("SECRET") + token = jwt.encode(payload=payload, key=key) + return token diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index 31736f4..8139113 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -3,14 +3,14 @@ # Case 1: Successful delete a bid by changing status to deleted @patch("api.controllers.bid_controller.db") -def test_delete_bid_success(mock_db, test_client, api_key): +def test_delete_bid_success(mock_db, test_client, admin_jwt): mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "deleted", } response = test_client.delete( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"X-API-Key": api_key}, + headers={"Authorization": f"Bearer {admin_jwt}"}, ) assert response.status_code == 204 assert response.content_length is None @@ -18,11 +18,11 @@ def test_delete_bid_success(mock_db, test_client, api_key): # Case 2: Failed to call database @patch("api.controllers.bid_controller.db") -def test_delete_bid_connection_error(mock_db, test_client, api_key): +def test_delete_bid_connection_error(mock_db, test_client, admin_jwt): mock_db["bids"].find_one_and_update.side_effect = Exception response = test_client.delete( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"X-API-Key": api_key}, + headers={"Authorization": f"Bearer {admin_jwt}"}, ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -30,9 +30,9 @@ def test_delete_bid_connection_error(mock_db, test_client, api_key): # Case 3: Validation error @patch("api.controllers.bid_controller.db") -def test_delete_bid_validation_error(mock_db, test_client, api_key): +def test_delete_bid_validation_error(mock_db, test_client, admin_jwt): response = test_client.delete( - "/api/bids/invalid_bid_id", headers={"X-API-Key": api_key} + "/api/bids/invalid_bid_id", headers={"Authorization": f"Bearer {admin_jwt}"} ) assert response.status_code == 400 assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} @@ -40,12 +40,12 @@ def test_delete_bid_validation_error(mock_db, test_client, api_key): # Case 4: Bid not found @patch("api.controllers.bid_controller.db") -def test_delete_bid_not_found(mock_db, test_client, api_key): +def test_delete_bid_not_found(mock_db, test_client, admin_jwt): mock_db["bids"].find_one_and_update.return_value = None response = test_client.delete( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"X-API-Key": api_key}, + headers={"Authorization": f"Bearer {admin_jwt}"}, ) mock_db["bids"].find_one_and_update.assert_called_once() @@ -53,16 +53,16 @@ def test_delete_bid_not_found(mock_db, test_client, api_key): assert response.get_json() == {"Error": "Resource not found"} -# Case 5: Unauthorized - invalid API key +# Case 5: Unauthorized - invalid token @patch("api.controllers.bid_controller.db") -def test_delete_bid_unauthorized(mock_db, test_client, api_key): +def test_delete_bid_unauthorized(mock_db, test_client, admin_jwt): mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "deleted", } response = test_client.delete( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"X-API-Key": "INVALID"}, + headers={"Authorization": "Bearer N0tV4l1djsonW3Bt0K3n"}, ) assert response.status_code == 401 assert response.get_json() == {"Error": "Unauthorized"} diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index b1f7dd2..a236e74 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -21,7 +21,7 @@ def test_get_bid_by_id_success(mock_db, test_client, api_key): response = test_client.get( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", - headers={"host": "localhost:8080"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) mock_db["bids"].find_one.assert_called_once_with( @@ -48,7 +48,10 @@ def test_get_bid_by_id_success(mock_db, test_client, api_key): @patch("api.controllers.bid_controller.db", side_effect=Exception) def test_get_bid_by_id_connection_error(mock_db, test_client, api_key): mock_db["bids"].find_one.side_effect = Exception - response = test_client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + response = test_client.get( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", + headers={"host": "localhost:8080", "X-API-Key": api_key}, + ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -58,7 +61,10 @@ def test_get_bid_by_id_connection_error(mock_db, test_client, api_key): def test_get_bid_by_id_not_found(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = None - response = test_client.get("/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9") + response = test_client.get( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", + headers={"host": "localhost:8080", "X-API-Key": api_key}, + ) mock_db["bids"].find_one.assert_called_once_with( {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": {"$ne": "deleted"}} @@ -70,6 +76,9 @@ def test_get_bid_by_id_not_found(mock_db, test_client, api_key): # Case 4: Validation error @patch("api.controllers.bid_controller.db") def test_get_bid_by_id_validation_error(mock_db, test_client, api_key): - response = test_client.get("/api/bids/invalid_bid_id") + response = test_client.get( + "/api/bids/invalid_bid_id", + headers={"host": "localhost:8080", "X-API-Key": api_key}, + ) 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 f50aac1..e87ef30 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -6,7 +6,9 @@ def test_get_bids_success(mock_db, test_client, api_key): mock_db["bids"].find.return_value = [] - response = test_client.get("/api/bids") + response = test_client.get( + "/api/bids", headers={"host": "localhost:8080", "X-API-Key": api_key} + ) mock_db["bids"].find.assert_called_once_with({"status": {"$ne": "deleted"}}) assert response.status_code == 200 assert response.get_json() == {"total_count": 0, "items": []} @@ -29,7 +31,9 @@ def test_links_with_host(mock_db, test_client, api_key): } ] - response = test_client.get("/api/bids", headers={"host": "localhost:8080"}) + response = test_client.get( + "/api/bids", headers={"host": "localhost:8080", "X-API-Key": api_key} + ) assert response.status_code == 200 assert response.get_json() == { "total_count": 1, @@ -53,6 +57,8 @@ def test_links_with_host(mock_db, test_client, api_key): @patch("api.controllers.bid_controller.db") def test_get_bids_connection_error(mock_db, test_client, api_key): mock_db["bids"].find.side_effect = Exception - response = test_client.get("/api/bids") + response = test_client.get( + "/api/bids", headers={"host": "localhost:8080", "X-API-Key": api_key} + ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} diff --git a/tests/test_phase_schema.py b/tests/test_phase_schema.py index b6cec4b..55ead69 100644 --- a/tests/test_phase_schema.py +++ b/tests/test_phase_schema.py @@ -3,7 +3,7 @@ # Case 1: score is mandatory when has_score is set to True @patch("api.controllers.bid_controller.db") -def test_score_is_mandatory(mock_db, test_client, api_key): +def test_score_is_mandatory(mock_db, test_client, basic_jwt): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -11,7 +11,9 @@ def test_score_is_mandatory(mock_db, test_client, api_key): "success": [{"phase": 1, "has_score": True, "out_of": 36}], } - response = test_client.post("api/bids", json=data, headers={"X-API-Key": api_key}) + response = test_client.post( + "api/bids", json=data, headers={"Authorization": f"Bearer {basic_jwt}"} + ) assert response.status_code == 400 assert ( response.get_json()["Error"] @@ -21,7 +23,7 @@ def test_score_is_mandatory(mock_db, test_client, api_key): # Case 2: out_of is mandatory when has_score is set to True @patch("api.controllers.bid_controller.db") -def test_out_of_is_mandatory(mock_db, test_client, api_key): +def test_out_of_is_mandatory(mock_db, test_client, basic_jwt): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -29,7 +31,9 @@ def test_out_of_is_mandatory(mock_db, test_client, api_key): "failed": {"phase": 2, "has_score": True, "score": 20}, } - response = test_client.post("api/bids", json=data, headers={"X-API-Key": api_key}) + response = test_client.post( + "api/bids", json=data, headers={"Authorization": f"Bearer {basic_jwt}"} + ) assert response.status_code == 400 assert ( response.get_json()["Error"] diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index b39295f..80e9b2d 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -3,7 +3,7 @@ # Case 1: Successful post @patch("api.controllers.bid_controller.db") -def test_post_is_successful(mock_db, test_client, api_key): +def test_post_is_successful(mock_db, test_client, basic_jwt): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -21,7 +21,9 @@ def test_post_is_successful(mock_db, test_client, api_key): # Mock the behavior of db mock_db["bids"].insert_one.return_value = data - response = test_client.post("api/bids", json=data, headers={"X-API-Key": api_key}) + response = test_client.post( + "api/bids", json=data, headers={"Authorization": f"Bearer {basic_jwt}"} + ) assert response.status_code == 201 assert "_id" in response.get_json() and response.get_json()["_id"] is not None assert ( @@ -45,10 +47,12 @@ def test_post_is_successful(mock_db, test_client, api_key): # Case 2: Missing mandatory fields @patch("api.controllers.bid_controller.db") -def test_field_missing(mock_db, test_client, api_key): +def test_field_missing(mock_db, test_client, basic_jwt): data = {"client": "Sample Client", "bid_date": "2023-06-20"} - response = test_client.post("api/bids", json=data, headers={"X-API-Key": api_key}) + response = test_client.post( + "api/bids", json=data, headers={"Authorization": f"Bearer {basic_jwt}"} + ) assert response.status_code == 400 assert response.get_json() == { "Error": "{'tender': {'message': 'Missing mandatory field'}}" @@ -57,7 +61,7 @@ def test_field_missing(mock_db, test_client, api_key): # Case 3: Connection error @patch("api.controllers.bid_controller.db") -def test_post_bid_connection_error(mock_db, test_client, api_key): +def test_post_bid_connection_error(mock_db, test_client, basic_jwt): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -73,15 +77,17 @@ def test_post_bid_connection_error(mock_db, test_client, api_key): } # Mock the behavior of db mock_db["bids"].insert_one.side_effect = Exception - response = test_client.post("/api/bids", json=data, headers={"X-API-Key": api_key}) + response = test_client.post( + "/api/bids", json=data, headers={"Authorization": f"Bearer {basic_jwt}"} + ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} -# Case 4: Unauthorized - invalid API key +# Case 4: Unauthorized - invalid token @patch("api.controllers.bid_controller.db") -def test_post_bid_unauthorized(mock_db, test_client, api_key): +def test_post_bid_unauthorized(mock_db, test_client): data = { "tender": "Business Intelligence and Data Warehousing", "client": "Office for National Statistics", @@ -98,6 +104,8 @@ def test_post_bid_unauthorized(mock_db, test_client, api_key): # Mock the behavior of db mock_db["bids"].insert_one.return_value = data - response = test_client.post("api/bids", json=data, headers={"X-API-Key": "INVALID"}) + response = test_client.post( + "api/bids", json=data, headers={"Authorization": "Bearer N0tV4l1djsonW3Bt0K3n"} + ) assert response.status_code == 401 assert response.get_json() == {"Error": "Unauthorized"} diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index a3a3d36..0beceab 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -3,7 +3,7 @@ # Case 1: Successful update @patch("api.controllers.bid_controller.db") -def test_update_bid_by_id_success(mock_db, test_client, api_key): +def test_update_bid_by_id_success(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", @@ -17,7 +17,9 @@ def test_update_bid_by_id_success(mock_db, test_client, api_key): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "UPDATED TENDER"} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}", + json=update, + headers={"Authorization": f"Bearer {basic_jwt}"}, ) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} @@ -28,7 +30,7 @@ def test_update_bid_by_id_success(mock_db, test_client, api_key): # Case 2: Invalid user input @patch("api.controllers.bid_controller.db") -def test_input_validation(mock_db, test_client, api_key): +def test_input_validation(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -50,7 +52,9 @@ def test_input_validation(mock_db, test_client, api_key): bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"tender": 42} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}", + json=update, + headers={"Authorization": f"Bearer {basic_jwt}"}, ) assert response.status_code == 400 assert response.get_json()["Error"] == "{'tender': ['Not a valid string.']}" @@ -58,12 +62,14 @@ def test_input_validation(mock_db, test_client, api_key): # Case 3: Bid not found @patch("api.controllers.bid_controller.db") -def test_bid_not_found(mock_db, test_client, api_key): +def test_bid_not_found(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}", + json=update, + headers={"Authorization": f"Bearer {basic_jwt}"}, ) assert response.status_code == 404 assert response.get_json()["Error"] == "Resource not found" @@ -71,11 +77,13 @@ def test_bid_not_found(mock_db, test_client, api_key): # Case 4: Cannot update status @patch("api.controllers.bid_controller.db") -def test_cannot_update_status(mock_db, test_client, api_key): +def test_cannot_update_status(mock_db, test_client, basic_jwt): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "deleted"} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}", + json=update, + headers={"Authorization": f"Bearer {basic_jwt}"}, ) assert response.status_code == 422 assert response.get_json()["Error"] == "Cannot update status" @@ -83,7 +91,7 @@ def test_cannot_update_status(mock_db, test_client, api_key): # Case 5: Failed to call database @patch("api.controllers.bid_controller.db") -def test_update_by_id_find_error(mock_db, test_client, api_key): +def test_update_by_id_find_error(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", @@ -97,7 +105,9 @@ def test_update_by_id_find_error(mock_db, test_client, api_key): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "Updated tender"} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}", + json=update, + headers={"Authorization": f"Bearer {basic_jwt}"}, ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} @@ -105,7 +115,7 @@ def test_update_by_id_find_error(mock_db, test_client, api_key): # Case 6: Update failed field @patch("api.controllers.bid_controller.db") -def test_update_failed(mock_db, test_client, api_key): +def test_update_failed(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -127,7 +137,9 @@ def test_update_failed(mock_db, test_client, api_key): bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"failed": {"phase": 2, "has_score": True, "score": 20, "out_of": 36}} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}", + json=update, + headers={"Authorization": f"Bearer {basic_jwt}"}, ) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} @@ -138,7 +150,7 @@ def test_update_failed(mock_db, test_client, api_key): # Case 7: Update success field @patch("api.controllers.bid_controller.db") -def test_update_success(mock_db, test_client, api_key): +def test_update_success(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -165,7 +177,9 @@ def test_update_success(mock_db, test_client, api_key): ] } response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}", + json=update, + headers={"Authorization": f"Bearer {basic_jwt}"}, ) mock_db["bids"].find_one.assert_called_once_with( {"_id": bid_id, "status": "in_progress"} @@ -174,9 +188,9 @@ def test_update_success(mock_db, test_client, api_key): assert response.status_code == 200 -# Case 8: Unauthorized - invalid API key +# Case 8: Unauthorized - invalid token @patch("api.controllers.bid_controller.db") -def test_update_bid_by_id_unauthorized(mock_db, test_client, api_key): +def test_update_bid_by_id_unauthorized(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", "tender": "Business Intelligence and Data Warehousing", @@ -190,7 +204,9 @@ def test_update_bid_by_id_unauthorized(mock_db, test_client, api_key): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"tender": "UPDATED TENDER"} response = test_client.put( - f"api/bids/{bid_id}", json=update, headers={"X-API-Key": "INVALID"} + f"api/bids/{bid_id}", + json=update, + headers={"Authorization": "Bearer N0tV4l1djsonW3Bt0K3n"}, ) assert response.status_code == 401 assert response.get_json() == {"Error": "Unauthorized"} diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index 13e4d77..5657786 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -3,7 +3,7 @@ # Case 1: Successful update @patch("api.controllers.bid_controller.db") -def test_update_bid_status_success(mock_db, test_client, api_key): +def test_update_bid_status_success(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -26,7 +26,9 @@ def test_update_bid_status_success(mock_db, test_client, api_key): bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"status": "completed"} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}/status", + json=update, + headers={"Authorization": f"Bearer {admin_jwt}"}, ) mock_db["bids"].find_one.assert_called_once_with({"_id": bid_id}) mock_db["bids"].replace_one.assert_called_once() @@ -35,7 +37,7 @@ def test_update_bid_status_success(mock_db, test_client, api_key): # Case 2: Invalid status @patch("api.controllers.bid_controller.db") -def test_invalid_status(mock_db, test_client, api_key): +def test_invalid_status(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -57,7 +59,9 @@ def test_invalid_status(mock_db, test_client, api_key): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "invalid"} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}/status", + json=update, + headers={"Authorization": f"Bearer {admin_jwt}"}, ) assert response.status_code == 400 assert ( @@ -68,11 +72,13 @@ def test_invalid_status(mock_db, test_client, api_key): # Case 3: Empty request body @patch("api.controllers.bid_controller.db") -def test_empty_request(mock_db, test_client, api_key): +def test_empty_request(mock_db, test_client, admin_jwt): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}/status", + json=update, + headers={"Authorization": f"Bearer {admin_jwt}"}, ) assert response.status_code == 422 assert response.get_json()["Error"] == "Request must not be empty" @@ -80,12 +86,14 @@ def test_empty_request(mock_db, test_client, api_key): # Case 4: Bid not found @patch("api.controllers.bid_controller.db") -def test_bid_not_found(mock_db, test_client, api_key): +def test_bid_not_found(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}/status", + json=update, + headers={"Authorization": f"Bearer {admin_jwt}"}, ) assert response.status_code == 404 assert response.get_json()["Error"] == "Resource not found" @@ -93,20 +101,22 @@ def test_bid_not_found(mock_db, test_client, api_key): # Case 5: Failed to call database @patch("api.controllers.bid_controller.db") -def test_update_status_find_error(mock_db, test_client, api_key): +def test_update_status_find_error(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.side_effect = Exception bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "completed"} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": api_key} + f"api/bids/{bid_id}/status", + json=update, + headers={"Authorization": f"Bearer {admin_jwt}"}, ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} -# Case 6: Unauthorized - invalid API key +# Case 6: Unauthorized - invalid token @patch("api.controllers.bid_controller.db") -def test_update_bid_status_unauthorized(mock_db, test_client, api_key): +def test_update_bid_status_unauthorized(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", @@ -129,7 +139,9 @@ def test_update_bid_status_unauthorized(mock_db, test_client, api_key): bid_id = "4141fac8-8879-4169-a46d-2effb1f515f6" update = {"status": "completed"} response = test_client.put( - f"api/bids/{bid_id}/status", json=update, headers={"X-API-Key": "INVALID"} + f"api/bids/{bid_id}/status", + json=update, + headers={"Authorization": "Bearer N0tV4l1djsonW3Bt0K3n"}, ) assert response.status_code == 401 assert response.get_json() == {"Error": "Unauthorized"} From 53f60f1c7372539e08f523b367b4319a33a73a0a Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 28 Jul 2023 12:42:20 +0100 Subject: [PATCH 120/208] test: added tests for forbidden; created script to test delete with admin; added gmake targets --- Makefile | 11 ++++++++- scripts/post_and_delete_admin.py | 40 ++++++++++++++++++++++++++++++++ scripts/post_bid_jwt.py | 2 +- tests/test_get_bids.py | 10 ++++++++ tests/test_update_bid_status.py | 33 ++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 scripts/post_and_delete_admin.py diff --git a/Makefile b/Makefile index d29c2de..c1fdb7f 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 setup bids dbclean +.PHONY: run test clean check help commit swagger format branch lint setup bids dbclean auth authg authp authpd help: @echo "gmake help - display this help" @@ -30,6 +30,15 @@ help: auth: $(PYTHON) ../tdse-accessForce-auth-api/app.py +authg: + @find . -name "get_bids_apikey.py" -exec python3 {} \; + +authp: + @find . -name "post_bid_jwt.py" -exec python3 {} \; + +authpd: + @find . -name "post_and_delete_admin.py" -exec python3 {} \; + bids: @echo "Creating sample data..." @find . -name "create_sample_data.py" -exec python3 {} \; diff --git a/scripts/post_and_delete_admin.py b/scripts/post_and_delete_admin.py new file mode 100644 index 0000000..25a9472 --- /dev/null +++ b/scripts/post_and_delete_admin.py @@ -0,0 +1,40 @@ +import json +import requests + + +user_info = {"username": "Pira"} + +response = requests.post( + "http://localhost:5000/authorise/", + data=json.dumps(user_info), + headers={"Content-Type": "application/json"}, +) + +token = response.json()["jwt"] + +# print(token) + +data = { + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", +} + +fstring = f"Bearer {token}" + +headers = {"Content-Type": "application/json", "Authorization": fstring} + +post_response = requests.post( + "http://localhost:8080/api/bids", data=json.dumps(data), headers=headers +) + +# print(post_response) + +bid_id = post_response.json()["_id"] + +delete_url = f"http://localhost:8080/api/bids/{bid_id}" + +delete_response = requests.delete(delete_url, headers=headers) + +print(delete_response) diff --git a/scripts/post_bid_jwt.py b/scripts/post_bid_jwt.py index 103dd12..29e3f7e 100644 --- a/scripts/post_bid_jwt.py +++ b/scripts/post_bid_jwt.py @@ -2,7 +2,7 @@ import requests -user_info = {"username": "Tester McTestface", "admin": False} +user_info = {"username": "Tester McTestface"} response = requests.post( "http://localhost:5000/authorise/", diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index e87ef30..58cacd8 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -62,3 +62,13 @@ def test_get_bids_connection_error(mock_db, test_client, api_key): ) assert response.status_code == 500 assert response.get_json() == {"Error": "Could not connect to database"} + + +# Case 4: Unauthorized / invalid api key +@patch("api.controllers.bid_controller.db") +def test_get_bids_unauthorized(mock_db, test_client): + response = test_client.get( + "/api/bids", headers={"host": "localhost:8080", "X-API-Key": "INVALID_API_KEY"} + ) + assert response.status_code == 401 + assert response.get_json()["Error"] == "Unauthorized" diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index 5657786..b23706d 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -145,3 +145,36 @@ def test_update_bid_status_unauthorized(mock_db, test_client, admin_jwt): ) assert response.status_code == 401 assert response.get_json() == {"Error": "Unauthorized"} + + +# Case 7: Forbidden - not admin +@patch("api.controllers.bid_controller.db") +def test_update_bid_status_forbidden(mock_db, test_client, basic_jwt): + 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 = {"status": "completed"} + response = test_client.put( + f"api/bids/{bid_id}/status", + json=update, + headers={"Authorization": f"Bearer {basic_jwt}"}, + ) + assert response.status_code == 403 + assert response.get_json() == {"Error": "Forbidden"} From 2fbd6452785c10814e5f41a4c5d7bcf27cdc8c69 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 28 Jul 2023 14:17:19 +0100 Subject: [PATCH 121/208] fix: removed host prefix from links in test data --- scripts/test_data/bids.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/test_data/bids.json b/scripts/test_data/bids.json index caf934e..8c35cf0 100644 --- a/scripts/test_data/bids.json +++ b/scripts/test_data/bids.json @@ -18,8 +18,8 @@ }, "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" + "questions": "/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + "self": "/bids/be15c306-c85b-4e67-a9f6-682553c065a1" }, "status": "in_progress", "success": [ @@ -51,8 +51,8 @@ }, "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" + "questions": "/bids/b4846631-9135-4208-8e37-70eba8f77e15/questions", + "self": "/bids/b4846631-9135-4208-8e37-70eba8f77e15" }, "status": "in_progress", "success": [ @@ -84,8 +84,8 @@ }, "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" + "questions": "/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e/questions", + "self": "/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e" }, "status": "in_progress", "success": [ From 2dd8245aba8fc3e85ba23bf5f3ac197d113189aa Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 28 Jul 2023 16:28:24 +0100 Subject: [PATCH 122/208] test: wip - convert api interaction scripts to integration tests --- requirements.txt | 3 ++- scripts/__init__.py | 0 {scripts => tests/integration_tests}/get_bids_apikey.py | 4 ++-- {scripts => tests/integration_tests}/post_and_delete_admin.py | 0 {scripts => tests/integration_tests}/post_bid_jwt.py | 0 5 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 scripts/__init__.py rename {scripts => tests/integration_tests}/get_bids_apikey.py (81%) rename {scripts => tests/integration_tests}/post_and_delete_admin.py (100%) rename {scripts => tests/integration_tests}/post_bid_jwt.py (100%) diff --git a/requirements.txt b/requirements.txt index 838fee4..354f071 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,5 @@ pylint coverage pytest sphinx -sphinx-rtd-theme \ No newline at end of file +sphinx-rtd-theme +pyjwt \ No newline at end of file diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/get_bids_apikey.py b/tests/integration_tests/get_bids_apikey.py similarity index 81% rename from scripts/get_bids_apikey.py rename to tests/integration_tests/get_bids_apikey.py index 7d42aca..1160c79 100644 --- a/scripts/get_bids_apikey.py +++ b/tests/integration_tests/get_bids_apikey.py @@ -15,6 +15,6 @@ headers = {"Content-Type": "application/json", "X-API-Key": api_key} -post_response = requests.get("http://localhost:8080/api/bids", headers=headers) +get_response = requests.get("http://localhost:8080/api/bids", headers=headers) -print(post_response) +print(get_response) diff --git a/scripts/post_and_delete_admin.py b/tests/integration_tests/post_and_delete_admin.py similarity index 100% rename from scripts/post_and_delete_admin.py rename to tests/integration_tests/post_and_delete_admin.py diff --git a/scripts/post_bid_jwt.py b/tests/integration_tests/post_bid_jwt.py similarity index 100% rename from scripts/post_bid_jwt.py rename to tests/integration_tests/post_bid_jwt.py From 464397ef8e2f8620b868b72475ed843dfcc65680 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 31 Jul 2023 14:49:57 +0100 Subject: [PATCH 123/208] test: added delete_forbidden test; updated testconfig; changed SECRET to SECRET_KEY --- helpers/helpers.py | 3 ++- tests/conftest.py | 4 ++-- tests/test_delete_bid.py | 17 ++++++++++++++++- tests/test_update_bid_status.py | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/helpers/helpers.py b/helpers/helpers.py index ad38d4d..58351af 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -133,9 +133,10 @@ def wrapper(*args, **kwargs): def validate_token(request): PREFIX = "Bearer " auth_header = request.headers.get("Authorization") + assert auth_header is not None assert auth_header.startswith(PREFIX) is True token = auth_header[len(PREFIX) :] load_dotenv() - key = os.getenv("SECRET") + key = os.getenv("SECRET_KEY") decoded = jwt.decode(token, key, algorithms="HS256") return decoded diff --git a/tests/conftest.py b/tests/conftest.py index 21445b1..4df9b73 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,7 @@ def api_key(): def basic_jwt(): payload = {"username": "User McTestface", "admin": False} load_dotenv() - key = os.getenv("SECRET") + key = os.getenv("SECRET_KEY") token = jwt.encode(payload=payload, key=key) return token @@ -34,6 +34,6 @@ def basic_jwt(): def admin_jwt(): payload = {"username": "Admin McTestface", "admin": True} load_dotenv() - key = os.getenv("SECRET") + key = os.getenv("SECRET_KEY") token = jwt.encode(payload=payload, key=key) return token diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index 8139113..c3233e5 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -55,7 +55,7 @@ def test_delete_bid_not_found(mock_db, test_client, admin_jwt): # Case 5: Unauthorized - invalid token @patch("api.controllers.bid_controller.db") -def test_delete_bid_unauthorized(mock_db, test_client, admin_jwt): +def test_delete_bid_unauthorized(mock_db, test_client): mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "deleted", @@ -66,3 +66,18 @@ def test_delete_bid_unauthorized(mock_db, test_client, admin_jwt): ) assert response.status_code == 401 assert response.get_json() == {"Error": "Unauthorized"} + + +# Case 6: Forbidden - not admin +@patch("api.controllers.bid_controller.db") +def test_delete_bid_forbidden(mock_db, test_client, basic_jwt): + mock_db["bids"].find_one_and_update.return_value = { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "status": "deleted", + } + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", + headers={"Authorization": f"Bearer {basic_jwt}"}, + ) + assert response.status_code == 403 + assert response.get_json() == {"Error": "Forbidden"} diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index b23706d..733e857 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -116,7 +116,7 @@ def test_update_status_find_error(mock_db, test_client, admin_jwt): # Case 6: Unauthorized - invalid token @patch("api.controllers.bid_controller.db") -def test_update_bid_status_unauthorized(mock_db, test_client, admin_jwt): +def test_update_bid_status_unauthorized(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", "alias": "ONS", From 697c6207fe3a1d3e8e370ce3ebdf9004b79fb759 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 31 Jul 2023 17:16:32 +0100 Subject: [PATCH 124/208] docs: updated swagger for security schemes and responses --- static/swagger_config.yml | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 16dc336..14153d0 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -47,6 +47,8 @@ paths: - bids summary: Returns all bids description: A JSON with item count and array of all bids + security: + - ApiKeyAuth: [] responses: '200': # status code description: Successful operation @@ -62,6 +64,8 @@ paths: type: array items: $ref: '#/components/schemas/Bid' + '401': + $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' # -------------------------------------------- @@ -74,6 +78,8 @@ paths: requestBody: $ref: '#/components/requestBodies/PostBid' required: true + security: + - BearerAuth: [] responses: '201': description: Created @@ -83,6 +89,8 @@ paths: $ref: '#/components/schemas/Bid' '400': $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' # -------------------------------------------- @@ -102,6 +110,8 @@ paths: schema: type: string format: uuid + security: + - ApiKeyAuth: [] responses: '200': description: A single bid @@ -111,6 +121,8 @@ paths: $ref: '#/components/schemas/Bid' '400': $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '500': @@ -133,6 +145,8 @@ paths: requestBody: $ref: '#/components/requestBodies/UpdateBid' required: true + security: + - BearerAuth: [] responses: '200': description: Successful operation @@ -142,6 +156,8 @@ paths: $ref: '#/components/schemas/Bid' '400': $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '422': @@ -163,6 +179,8 @@ paths: schema: type: string format: uuid + security: + - BearerAuth: [] responses: # return 204 (No Content) '204': @@ -171,6 +189,10 @@ paths: noContent: {} '400': $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '500': @@ -195,6 +217,8 @@ paths: requestBody: $ref: '#/components/requestBodies/UpdateBid' required: true + security: + - BearerAuth: [] responses: '200': description: Successful operation @@ -204,6 +228,10 @@ paths: $ref: '#/components/schemas/Bid' '400': $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': @@ -374,6 +402,15 @@ components: type: array items: $ref: '#/components/schemas/Phase' + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT # -------------------------------------------- # Request bodies requestBodies: @@ -480,6 +517,15 @@ components: example: { "Error": "{'{field}': ['{message}']}" } + Forbidden: + description: Forbidden + content: + application/json: + schema: + type: object + example: { + "Error": "Forbidden" + } NotFound: description: Not Found Error content: @@ -498,6 +544,15 @@ components: example: { "Error": "Could not connect to database" } + Unauthorized: + description: Unauthorized - invalid or missing API key / token + content: + application/json: + schema: + type: object + example: { + "Error": "Unauthorised" + } UnprocessableEntity: description: Unprocessable Entity content: From b3195c04bb771852e7242d44dadc019e5e33bf4f Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 31 Jul 2023 19:38:24 +0100 Subject: [PATCH 125/208] feat: auth script to interact with different levels of access --- Makefile | 4 + request_examples/all_fields.http | 2 +- request_examples/get_jwt_from_auth.http | 3 +- scripts/auth.py | 140 ++++++++++++++++++++++++ scripts/get_bids_apikey.py | 20 ---- scripts/post_and_delete_admin.py | 40 ------- scripts/post_bid_jwt.py | 32 ------ 7 files changed, 146 insertions(+), 95 deletions(-) create mode 100644 scripts/auth.py delete mode 100644 scripts/get_bids_apikey.py delete mode 100644 scripts/post_and_delete_admin.py delete mode 100644 scripts/post_bid_jwt.py diff --git a/Makefile b/Makefile index c1fdb7f..2c89365 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,10 @@ test: @echo "TEST COVERAGE REPORT" coverage report -m --omit="tests/*,dbconfig/*" +token: + @echo "Getting JWT..." + @find . -name "get_jwt.py" -exec python3 {} \; + venv/bin/activate: requirements.txt python3 -m venv .venv $(PIP) install -r requirements.txt diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index d2de3ea..b19effa 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -1,6 +1,6 @@ POST http://localhost:8080/api/bids HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRlc3RlciBNY1Rlc3RmYWNlIiwiYWRtaW4iOmZhbHNlfQ.Dg7f8LVtALYWvjZH31re5C-Pc6Hp6Ra-U4LAy0ZQQ9M +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJhIiwiYWRtaW4iOmZhbHNlLCJleHAiOjE2OTE0MjAxODZ9.FuanolVVEPL32xDTJfnqj7w5GEOoGI5qNu2nILmxTfM { "tender": "Business Intelligence and Data Warehousing", diff --git a/request_examples/get_jwt_from_auth.http b/request_examples/get_jwt_from_auth.http index 559d2d3..4b74f82 100644 --- a/request_examples/get_jwt_from_auth.http +++ b/request_examples/get_jwt_from_auth.http @@ -2,6 +2,5 @@ POST http://localhost:5000/authorise/ HTTP/1.1 Content-Type: application/json { - "username": "Tester McTestface", - "admin": false + "username": "Julio" } diff --git a/scripts/auth.py b/scripts/auth.py new file mode 100644 index 0000000..4417451 --- /dev/null +++ b/scripts/auth.py @@ -0,0 +1,140 @@ +import requests + + +def simulate_login(username): + try: + payload = {"username": username} + response = requests.post("http://localhost:5000/authorise", json=payload) + response.raise_for_status() # Raise an exception for any HTTP errors + + token_data = response.json() + token = token_data + + return token + except requests.exceptions.RequestException as e: + print("Request Error:", e) + return None + except ValueError as e: + print("Value Error:", e) + return None + + +def post_bid(jwt_token): + try: + tender = input("Enter the tender: ") + client = input("Enter the client: ") + bid_date = input("Enter the bid date (YYYY-MM-DD): ") + bid_folder_url = input("Enter the bid folder URL(https://path-here): ") + payload = { + "tender": tender, + "client": client, + "bid_date": bid_date, + "bid_folder_url": bid_folder_url, + } + headers = {"Authorization": f"Bearer {jwt_token}"} + response = requests.post( + "http://localhost:8080/api/bids", json=payload, headers=headers + ) + response.raise_for_status() # Raise an exception for any HTTP errors + + data = response.json() + print("Post Success (id):", data["_id"]) + except requests.exceptions.RequestException as e: + print("Request Error:", e) + return None + + +def delete_bid(jwt_token): + try: + bid_id = input("Enter the bid ID to delete: ") + headers = {"Authorization": f"Bearer {jwt_token}"} + response = requests.delete( + f"http://localhost:8080/api/bids/{bid_id}", headers=headers + ) + response.raise_for_status() # Raise an exception for any HTTP errors + + print(f"Bid with ID {bid_id} deleted successfully.") + except requests.exceptions.RequestException as e: + print("Request Error:", e) + + +def find_bid_by_id(): + try: + API_KEY = input("Enter the API key (THIS_IS_THE_API_KEY): ") + headers = {"X-API-Key": API_KEY} + bid_id = input("Enter the bid ID to find: ") + response = requests.get( + f"http://localhost:8080/api/bids/{bid_id}", headers=headers + ) + response.raise_for_status() # Raise an exception for any HTTP errors + + bid_data = response.json() + print("Bid Data:") + print(bid_data) + except requests.exceptions.RequestException as e: + print("Request Error:", e) + + +def find_all_bids(): + try: + API_KEY = input("Enter the API key (THIS_IS_THE_API_KEY): ") + headers = {"X-API-Key": API_KEY} + response = requests.get("http://localhost:8080/api/bids", headers=headers) + response.raise_for_status() # Raise an exception for any HTTP errors + + all_bids_data = response.json() + print("All Bids Data:") + print(all_bids_data) + except requests.exceptions.RequestException as e: + print("Request Error:", e) + + +def access_level(admin): + admin_users = ["Julio", "Pira", "Nathan"] + if admin in admin_users: + print("You are an admin user.") + else: + print("You are not an admin user.") + + +def main(): + menu_choices = [ + "token", + "post", + "delete", + "find id", + "find all", + "access level", + "exit", + ] + username = input("Enter the username for login: ") + jwt_token = simulate_login(username) + while True: + if jwt_token: + print("Menu:") + for i, choice in enumerate(menu_choices): + print(f"{i + 1}. {choice}") + choice = input("Enter your choice: ") + if choice == "1": + print("JWT Token:", jwt_token) + elif choice == "2": + post_bid(jwt_token) + elif choice == "3": + delete_bid(jwt_token) + elif choice == "4": + find_bid_by_id() + elif choice == "5": + find_all_bids() + elif choice == "6": + access_level(username) + elif choice == "7": + print("Goodbye!") + break + else: + print("Invalid choice. Try again.") + else: + print("Login failed. Try again.") + + +if __name__ == "__main__": + main() diff --git a/scripts/get_bids_apikey.py b/scripts/get_bids_apikey.py deleted file mode 100644 index 7d42aca..0000000 --- a/scripts/get_bids_apikey.py +++ /dev/null @@ -1,20 +0,0 @@ -import requests - -response = requests.get("http://localhost:5000/authorise/") - -api_key = response.json()["API_KEY"] - -print(api_key) - -data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-06-23", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -} - -headers = {"Content-Type": "application/json", "X-API-Key": api_key} - -post_response = requests.get("http://localhost:8080/api/bids", headers=headers) - -print(post_response) diff --git a/scripts/post_and_delete_admin.py b/scripts/post_and_delete_admin.py deleted file mode 100644 index 25a9472..0000000 --- a/scripts/post_and_delete_admin.py +++ /dev/null @@ -1,40 +0,0 @@ -import json -import requests - - -user_info = {"username": "Pira"} - -response = requests.post( - "http://localhost:5000/authorise/", - data=json.dumps(user_info), - headers={"Content-Type": "application/json"}, -) - -token = response.json()["jwt"] - -# print(token) - -data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-06-23", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -} - -fstring = f"Bearer {token}" - -headers = {"Content-Type": "application/json", "Authorization": fstring} - -post_response = requests.post( - "http://localhost:8080/api/bids", data=json.dumps(data), headers=headers -) - -# print(post_response) - -bid_id = post_response.json()["_id"] - -delete_url = f"http://localhost:8080/api/bids/{bid_id}" - -delete_response = requests.delete(delete_url, headers=headers) - -print(delete_response) diff --git a/scripts/post_bid_jwt.py b/scripts/post_bid_jwt.py deleted file mode 100644 index 29e3f7e..0000000 --- a/scripts/post_bid_jwt.py +++ /dev/null @@ -1,32 +0,0 @@ -import json -import requests - - -user_info = {"username": "Tester McTestface"} - -response = requests.post( - "http://localhost:5000/authorise/", - data=json.dumps(user_info), - headers={"Content-Type": "application/json"}, -) - -token = response.json()["jwt"] - -print(token) - -data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-06-23", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -} - -fstring = f"Bearer {token}" - -headers = {"Content-Type": "application/json", "Authorization": fstring} - -post_response = requests.post( - "http://localhost:8080/api/bids", data=json.dumps(data), headers=headers -) - -print(post_response) From b3e0ddbc39c59585e816a4967c7eff665c80f6fb Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Mon, 31 Jul 2023 19:50:52 +0100 Subject: [PATCH 126/208] docs: authorization documentation --- Makefile | 20 ++++++-------------- README.md | 25 ++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 2c89365..e863b0e 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,8 @@ PIP = ./.venv/bin/pip help: @echo "gmake help - display this help" - @echo "gmake auth - run auth api application" + @echo "gmake authserver - run auth api application" + @echo "gmake auth - get JWT and interact with auth api" @echo "gmake bids - create sample data" @echo "gmake branch - create a new branch" @echo "gmake build - create and activate virtual environment" @@ -27,17 +28,12 @@ help: @echo "gmake setup - setup the application database" @echo "gmake test - run the tests" -auth: +authserver: $(PYTHON) ../tdse-accessForce-auth-api/app.py -authg: - @find . -name "get_bids_apikey.py" -exec python3 {} \; - -authp: - @find . -name "post_bid_jwt.py" -exec python3 {} \; - -authpd: - @find . -name "post_and_delete_admin.py" -exec python3 {} \; +auth: + @echo "Getting JWT..." + @find . -name "get_jwt.py" -exec python3 {} \; bids: @echo "Creating sample data..." @@ -106,10 +102,6 @@ test: @echo "TEST COVERAGE REPORT" coverage report -m --omit="tests/*,dbconfig/*" -token: - @echo "Getting JWT..." - @find . -name "get_jwt.py" -exec python3 {} \; - venv/bin/activate: requirements.txt python3 -m venv .venv $(PIP) install -r requirements.txt diff --git a/README.md b/README.md index 066bddd..0614596 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,17 @@ This API stores and serves information about Methods bids for client tenders. ```bash gmake run ``` -8. The API will be available at http://localhost:8080/api/bids + * The API will be available at http://localhost:8080/api/bids + +8. Follow this link to go to the authorization documentation: [Authorization Documentation](https://github.com/methods/tdse-accessForce-auth-stub/blob/main/README.md) + + +9. In a new terminal enter the following command to run authorization server if not already running. This will be needed to generate a token: + + ```bash + gmake authserver + ``` + * The API will be available at http://localhost:5000/authorise -------------- @@ -78,6 +88,19 @@ This API stores and serves information about Methods bids for client tenders. ``` -------------- +## Using auth playground to generate a token + +1. Follow the steps in the section above to start the API and authorization server. + +2. In a new terminal enter the following command to open the auth playground in your default web browser: + + ```bash + gmake auth + ``` +3. Follow the steps in the auth playground to generate a token and much more. + +-------------- + ## 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/) From 486ad65700018b0d43ba83077d5d010a7bc71194 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 1 Aug 2023 09:45:19 +0100 Subject: [PATCH 127/208] fix: name changed --- Makefile | 2 +- scripts/{auth.py => get_jwt.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename scripts/{auth.py => get_jwt.py} (99%) diff --git a/Makefile b/Makefile index e863b0e..b6b97c9 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ help: authserver: $(PYTHON) ../tdse-accessForce-auth-api/app.py -auth: +authplay: @echo "Getting JWT..." @find . -name "get_jwt.py" -exec python3 {} \; diff --git a/scripts/auth.py b/scripts/get_jwt.py similarity index 99% rename from scripts/auth.py rename to scripts/get_jwt.py index 4417451..e951816 100644 --- a/scripts/auth.py +++ b/scripts/get_jwt.py @@ -8,7 +8,7 @@ def simulate_login(username): response.raise_for_status() # Raise an exception for any HTTP errors token_data = response.json() - token = token_data + token = token_data['jwt'] return token except requests.exceptions.RequestException as e: From cb9ebb4b90de7d0c72235c6ab826beb8a253f365 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 1 Aug 2023 12:51:37 +0100 Subject: [PATCH 128/208] feat: added question model and related schemas; created helper validator function; implemented post question route --- Makefile | 2 +- api/controllers/question_controller.py | 40 ++++++++++++++ api/models/bid_model.py | 18 ++++++- api/models/links_model.py | 13 ----- api/models/question_model.py | 54 +++++++++++++++++++ .../{links_schema.py => bid_links_schema.py} | 2 +- api/schemas/bid_schema.py | 8 +-- api/schemas/question_links_schema.py | 14 +++++ api/schemas/question_schema.py | 36 +++++++++++++ app.py | 2 + helpers/helpers.py | 10 ++++ request_examples/all_fields.http | 11 ++-- scripts/get_jwt.py | 2 +- 13 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 api/controllers/question_controller.py delete mode 100644 api/models/links_model.py create mode 100644 api/models/question_model.py rename api/schemas/{links_schema.py => bid_links_schema.py} (91%) create mode 100644 api/schemas/question_links_schema.py create mode 100644 api/schemas/question_schema.py diff --git a/Makefile b/Makefile index b6b97c9..62033a7 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 setup bids dbclean auth authg authp authpd +.PHONY: run test clean check help commit swagger format branch lint setup bids dbclean authserver authplay help: @echo "gmake help - display this help" diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py new file mode 100644 index 0000000..35f2400 --- /dev/null +++ b/api/controllers/question_controller.py @@ -0,0 +1,40 @@ +from datetime import datetime +from flask import Blueprint, request +from marshmallow import ValidationError +from werkzeug.exceptions import UnprocessableEntity +from api.models.status_enum import Status +from dbconfig.mongo_setup import db +from helpers.helpers import ( + showInternalServerError, + showNotFoundError, + showUnprocessableEntityError, + showValidationError, + validate_and_create_question_document, + validate_bid_id_path, + validate_bid_update, + validate_status_update, + prepend_host_to_links, + require_api_key, + require_jwt, + require_admin_access, +) + +question = Blueprint("question", __name__) + + +@question.route("/bids//questions", methods=["POST"]) +# @require_jwt +def post_question(bid_id): + try: + bid_id = validate_bid_id_path(bid_id) + # Process input and create data model + data = validate_and_create_question_document(request.get_json(), bid_id) + # Insert document into database collection + db["questions"].insert_one(data) + return data, 201 + # Return 400 response if input validation fails + except ValidationError as e: + return showValidationError(e), 400 + # Return 500 response in case of connection failure + except Exception: + return showInternalServerError(), 500 diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 9be1259..5a12686 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -1,10 +1,9 @@ from uuid import uuid4 from datetime import datetime from api.models.status_enum import Status -from .links_model import LinksModel -# Description: Schema for the bid object +# Data model for bid resource class BidModel: """ Represents a bid model for the MongoDB database. @@ -57,3 +56,18 @@ def __init__( self.failed = failed self.feedback = feedback self.last_updated = datetime.now() + + +# Model for links object +class LinksModel: + """ + Represents a links model for the bid resource. + + Attributes: + self (str): The URL to the bid resource. + questions (str): The URL to the questions resource related to the bid. + """ + + def __init__(self, bid_id): + self.self = f"/bids/{bid_id}" + self.questions = f"/bids/{bid_id}/questions" diff --git a/api/models/links_model.py b/api/models/links_model.py deleted file mode 100644 index 91f4eec..0000000 --- a/api/models/links_model.py +++ /dev/null @@ -1,13 +0,0 @@ -# Schema for links object -class LinksModel: - """ - Represents a links model for the bid resource. - - Attributes: - self (str): The URL to the bid resource. - questions (str): The URL to the questions resource related to the bid. - """ - - def __init__(self, bid_id): - self.self = f"/bids/{bid_id}" - self.questions = f"/bids/{bid_id}/questions" diff --git a/api/models/question_model.py b/api/models/question_model.py new file mode 100644 index 0000000..2277cbe --- /dev/null +++ b/api/models/question_model.py @@ -0,0 +1,54 @@ +from uuid import uuid4 +from datetime import datetime +from api.models.status_enum import Status + + +# Data model for question resource +class QuestionModel: + def __init__( + self, + description, + question_url, + feedback, + bid_id, + response=None, + score=None, + out_of=None, + respondents=[], + status=None, + question_id=None, + links=None, + last_updated=None, + ): + if question_id is None: + self._id = uuid4() + else: + self._id = question_id + if status is None: + self.status = Status.IN_PROGRESS + else: + self.status = status + self.description = description + self.question_url = question_url + self.feedback = feedback + self.response = response + self.score = score + self.out_of = out_of + self.respondents = respondents + self.links = LinksModel(self._id, bid_id) + self.last_updated = datetime.now() + + +# Model for links object +class LinksModel: + """ + Represents a links model for the question resource. + + Attributes: + self (str): The URL to the question resource. + questions (str): The URL to the bid resource related to the question. + """ + + def __init__(self, question_id, bid_id): + self.self = f"/bids/{bid_id}/questions/{question_id}" + self.bid = f"/bids/{bid_id}" diff --git a/api/schemas/links_schema.py b/api/schemas/bid_links_schema.py similarity index 91% rename from api/schemas/links_schema.py rename to api/schemas/bid_links_schema.py index 6d84ec1..78d23da 100644 --- a/api/schemas/links_schema.py +++ b/api/schemas/bid_links_schema.py @@ -1,7 +1,7 @@ from marshmallow import Schema, fields -class LinksSchema(Schema): +class BidLinksSchema(Schema): """ Schema for representing links in a bid resource. diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 26467b8..6d21806 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,12 +1,12 @@ from marshmallow import Schema, fields, post_load, validates_schema, ValidationError from api.models.status_enum import Status from api.models.bid_model import BidModel -from .links_schema import LinksSchema +from .bid_links_schema import BidLinksSchema from .phase_schema import PhaseSchema from .feedback_schema import FeedbackSchema -# Marshmallow schema for request body +# Marshmallow schema for bid resource class BidSchema(Schema): """ Marshmallow schema for the bid object. @@ -23,7 +23,7 @@ class BidSchema(Schema): failed (PhaseSchema, optional): The failed phase of the bid. feedback (FeedbackSchema, optional): Feedback information for the bid. status (Status): The status of the bid (using the Status enum). - links (LinksSchema): Links to the bid resource and questions resource. + links (BidLinksSchema): Links to the bid resource and questions resource. last_updated (DateTime, optional): The date and time when the bid was last updated. """ @@ -48,7 +48,7 @@ class BidSchema(Schema): 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) + links = fields.Nested(BidLinksSchema) last_updated = fields.DateTime() @validates_schema diff --git a/api/schemas/question_links_schema.py b/api/schemas/question_links_schema.py new file mode 100644 index 0000000..aa56bfe --- /dev/null +++ b/api/schemas/question_links_schema.py @@ -0,0 +1,14 @@ +from marshmallow import Schema, fields + + +class QuestionLinksSchema(Schema): + """ + Schema for representing links in a question resource. + + Attributes: + self (str): The URL to the question resource. + questions (str): The URL to the bid resource related to the question. + """ + + self = fields.Str(required=True) + bid = fields.Str(required=True) diff --git a/api/schemas/question_schema.py b/api/schemas/question_schema.py new file mode 100644 index 0000000..f09864e --- /dev/null +++ b/api/schemas/question_schema.py @@ -0,0 +1,36 @@ +from marshmallow import Schema, fields, post_load +from api.models.status_enum import Status +from api.models.question_model import QuestionModel +from .question_links_schema import QuestionLinksSchema +from .feedback_schema import FeedbackSchema + + +# Marshmallow schema for question resource +class QuestionSchema(Schema): + _id = fields.UUID() + description = fields.Str( + required=True, + error_messages={"required": {"message": "Missing mandatory field"}}, + ) + question_url = fields.URL( + required=True, + error_messages={"required": {"message": "Missing mandatory field"}}, + ) + feedback = fields.Nested( + FeedbackSchema, + required=True, + error_messages={"required": {"message": "Missing mandatory field"}}, + ) + bid_id = fields.UUID(required=True) + response = fields.Str() + score = fields.Integer() + out_of = fields.Integer() + respondents = fields.List(fields.Str()) + status = fields.Enum(Status, by_value=True) + links = fields.Nested(QuestionLinksSchema) + last_updated = fields.DateTime() + + # Creates a Question instance after processing + @post_load + def makeQuestion(self, data, **kwargs): + return QuestionModel(**data) diff --git a/app.py b/app.py index 9d1930b..9af4347 100644 --- a/app.py +++ b/app.py @@ -6,6 +6,7 @@ from flask import Flask from flask_swagger_ui import get_swaggerui_blueprint from api.controllers.bid_controller import bid +from api.controllers.question_controller import question app = Flask(__name__) @@ -21,6 +22,7 @@ app.register_blueprint(swaggerui_blueprint) app.register_blueprint(bid, url_prefix="/api") +app.register_blueprint(question, url_prefix="/api") if __name__ == "__main__": diff --git a/helpers/helpers.py b/helpers/helpers.py index 58351af..865b659 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -9,6 +9,7 @@ from werkzeug.exceptions import UnprocessableEntity from api.schemas.bid_schema import BidSchema from api.schemas.bid_id_schema import BidIdSchema +from api.schemas.question_schema import QuestionSchema def showForbiddenError(): @@ -140,3 +141,12 @@ def validate_token(request): key = os.getenv("SECRET_KEY") decoded = jwt.decode(token, key, algorithms="HS256") return decoded + + +def validate_and_create_question_document(request, bid_id): + request["bid_id"] = bid_id + # Process input and create data model + question_document = QuestionSchema().load(request) + # Serialize to a JSON object + data = QuestionSchema().dump(question_document) + return data diff --git a/request_examples/all_fields.http b/request_examples/all_fields.http index b19effa..d7e8f57 100644 --- a/request_examples/all_fields.http +++ b/request_examples/all_fields.http @@ -1,10 +1,9 @@ -POST http://localhost:8080/api/bids HTTP/1.1 +POST http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJhIiwiYWRtaW4iOmZhbHNlLCJleHAiOjE2OTE0MjAxODZ9.FuanolVVEPL32xDTJfnqj7w5GEOoGI5qNu2nILmxTfM { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-06-23", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder" + "description": "This is a question", + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": {"description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder"} } diff --git a/scripts/get_jwt.py b/scripts/get_jwt.py index e951816..2a5e4e0 100644 --- a/scripts/get_jwt.py +++ b/scripts/get_jwt.py @@ -8,7 +8,7 @@ def simulate_login(username): response.raise_for_status() # Raise an exception for any HTTP errors token_data = response.json() - token = token_data['jwt'] + token = token_data["jwt"] return token except requests.exceptions.RequestException as e: From df6b65c6ed8c5fb03547dfb29cae4fe6eb25fe7d Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 1 Aug 2023 12:57:34 +0100 Subject: [PATCH 129/208] fix: restored post_bid request example; created new post_question example; renamed other examples to specify bid resource --- request_examples/{delete.http => delete_bid.http} | 0 request_examples/{get_all.http => get_all_bids.http} | 0 request_examples/post_bid.http | 10 ++++++++++ .../{all_fields.http => post_question.http} | 0 4 files changed, 10 insertions(+) rename request_examples/{delete.http => delete_bid.http} (100%) rename request_examples/{get_all.http => get_all_bids.http} (100%) create mode 100644 request_examples/post_bid.http rename request_examples/{all_fields.http => post_question.http} (100%) diff --git a/request_examples/delete.http b/request_examples/delete_bid.http similarity index 100% rename from request_examples/delete.http rename to request_examples/delete_bid.http diff --git a/request_examples/get_all.http b/request_examples/get_all_bids.http similarity index 100% rename from request_examples/get_all.http rename to request_examples/get_all_bids.http diff --git a/request_examples/post_bid.http b/request_examples/post_bid.http new file mode 100644 index 0000000..b19effa --- /dev/null +++ b/request_examples/post_bid.http @@ -0,0 +1,10 @@ +POST http://localhost:8080/api/bids HTTP/1.1 +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJhIiwiYWRtaW4iOmZhbHNlLCJleHAiOjE2OTE0MjAxODZ9.FuanolVVEPL32xDTJfnqj7w5GEOoGI5qNu2nILmxTfM + +{ + "tender": "Business Intelligence and Data Warehousing", + "client": "Office for National Statistics", + "bid_date": "2023-06-23", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder" +} diff --git a/request_examples/all_fields.http b/request_examples/post_question.http similarity index 100% rename from request_examples/all_fields.http rename to request_examples/post_question.http From 1a30c93c84b3d8d95ed7baaf153eca582b91c8a4 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 1 Aug 2023 14:28:44 +0100 Subject: [PATCH 130/208] test: added tests for question model; added tests for post question route and responses --- api/controllers/question_controller.py | 2 +- request_examples/post_question.http | 1 + tests/conftest.py | 2 + tests/test_post_question.py | 110 +++++++++++++++++++++++++ tests/test_question_schema.py | 107 ++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 tests/test_post_question.py create mode 100644 tests/test_question_schema.py diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 35f2400..9d11fdb 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -23,7 +23,7 @@ @question.route("/bids//questions", methods=["POST"]) -# @require_jwt +@require_jwt def post_question(bid_id): try: bid_id = validate_bid_id_path(bid_id) diff --git a/request_examples/post_question.http b/request_examples/post_question.http index d7e8f57..8778aa7 100644 --- a/request_examples/post_question.http +++ b/request_examples/post_question.http @@ -1,5 +1,6 @@ POST http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJhIiwiYWRtaW4iOmZhbHNlLCJleHAiOjE2OTE0MjAxODZ9.FuanolVVEPL32xDTJfnqj7w5GEOoGI5qNu2nILmxTfM { "description": "This is a question", diff --git a/tests/conftest.py b/tests/conftest.py index 4df9b73..ae6611d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,12 +4,14 @@ from dotenv import load_dotenv from flask import Flask from api.controllers.bid_controller import bid +from api.controllers.question_controller import question @pytest.fixture(scope="session") def test_client(): app = Flask(__name__) app.register_blueprint(bid, url_prefix="/api") + app.register_blueprint(question, url_prefix="/api") with app.test_client() as client: yield client diff --git a/tests/test_post_question.py b/tests/test_post_question.py new file mode 100644 index 0000000..f5ed2d9 --- /dev/null +++ b/tests/test_post_question.py @@ -0,0 +1,110 @@ +from unittest.mock import patch + + +# Case 1: Successful post +@patch("api.controllers.question_controller.db") +def test_post_is_successful(mock_db, test_client, basic_jwt): + data = { + "description": "This is a question", + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + + # Mock the behavior of db + mock_db["questions"].insert_one.return_value = data + + response = test_client.post( + "api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + json=data, + headers={"Authorization": f"Bearer {basic_jwt}"}, + ) + assert response.status_code == 201 + assert "_id" in response.get_json() and response.get_json()["_id"] is not None + assert ( + "description" in response.get_json() + and response.get_json()["description"] == "This is a question" + ) + assert ( + "question_url" in response.get_json() + and response.get_json()["question_url"] + == "https://organisation.sharepoint.com/Docs/dummyfolder" + ) + assert ( + "last_updated" in response.get_json() + and response.get_json()["last_updated"] is not None + ) + assert "feedback" in response.get_json() and response.get_json()["feedback"] == { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + } + + +# Case 2: Missing mandatory fields +@patch("api.controllers.question_controller.db") +def test_field_missing(mock_db, test_client, basic_jwt): + data = { + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + + response = test_client.post( + "api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + json=data, + headers={"Authorization": f"Bearer {basic_jwt}"}, + ) + assert response.status_code == 400 + assert response.get_json() == { + "Error": "{'description': {'message': 'Missing mandatory field'}}" + } + + +# Case 3: Connection error +@patch("api.controllers.question_controller.db") +def test_post_question_connection_error(mock_db, test_client, basic_jwt): + data = { + "description": "This is a question", + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + # Mock the behavior of db + mock_db["questions"].insert_one.side_effect = Exception + response = test_client.post( + "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + json=data, + headers={"Authorization": f"Bearer {basic_jwt}"}, + ) + + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} + + +# Case 4: Unauthorized - invalid token +@patch("api.controllers.question_controller.db") +def test_post_question_unauthorized(mock_db, test_client): + data = { + "description": "This is a question", + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + + # Mock the behavior of db + mock_db["questions"].insert_one.return_value = data + response = test_client.post( + "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + json=data, + headers={"Authorization": "Bearer N0tV4l1djsonW3Bt0K3n"}, + ) + assert response.status_code == 401 + assert response.get_json() == {"Error": "Unauthorized"} diff --git a/tests/test_question_schema.py b/tests/test_question_schema.py new file mode 100644 index 0000000..c82a429 --- /dev/null +++ b/tests/test_question_schema.py @@ -0,0 +1,107 @@ +import pytest +from marshmallow import ValidationError +from api.schemas.question_schema import QuestionSchema +from helpers.helpers import is_valid_uuid, is_valid_isoformat + + +# Case 1: New instance of bid model class generates expected fields +def test_question_model(): + data = { + "description": "This is a question", + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + bid_id = "be15c306-c85b-4e67-a9f6-682553c065a1" + data["bid_id"] = bid_id + bid_document = QuestionSchema().load(data) + to_post = QuestionSchema().dump(bid_document) + + question_id = to_post["_id"] + # Test that UUID is generated and is valid UUID + assert to_post["_id"] is not None + assert is_valid_uuid(question_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/{bid_id}/questions/{question_id}" + assert "bid" in to_post["links"] + assert to_post["links"]["bid"] == f"/bids/{bid_id}" + + # 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 - description +def test_validate_description(): + data = { + "description": 42, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + bid_id = "be15c306-c85b-4e67-a9f6-682553c065a1" + data["bid_id"] = bid_id + with pytest.raises(ValidationError): + QuestionSchema().load(data) + + +# Case 3: Field validation - question_url +def test_validate_description(): + data = { + "description": "This is a question", + "question_url": "Not a valid url", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + bid_id = "be15c306-c85b-4e67-a9f6-682553c065a1" + data["bid_id"] = bid_id + with pytest.raises(ValidationError): + QuestionSchema().load(data) + + +# Case 4: Field validation - feedback description +def test_validate_feedback_description(): + data = { + "description": "This is a question", + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": 42, + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + bid_id = "be15c306-c85b-4e67-a9f6-682553c065a1" + data["bid_id"] = bid_id + with pytest.raises(ValidationError): + QuestionSchema().load(data) + + +# Case 5: Field validation - feedback url +def test_validate_feedback_url(): + data = { + "description": "This is a question", + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Good feedback", + "url": "Not a valid url", + }, + } + bid_id = "be15c306-c85b-4e67-a9f6-682553c065a1" + data["bid_id"] = bid_id + with pytest.raises(ValidationError): + QuestionSchema().load(data) From 84e249ed51a5a70037fe2b11d387c42b830644d5 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 1 Aug 2023 14:39:27 +0100 Subject: [PATCH 131/208] feat: swagger documentation for questions --- README.md | 2 +- scripts/get_jwt.py | 2 +- static/swagger_config.yml | 339 +++++++++++++++++++++++++++++++++++++- 3 files changed, 337 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0614596..8d807d6 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ This API stores and serves information about Methods bids for client tenders. 2. In a new terminal enter the following command to open the auth playground in your default web browser: ```bash - gmake auth + gmake authplay ``` 3. Follow the steps in the auth playground to generate a token and much more. diff --git a/scripts/get_jwt.py b/scripts/get_jwt.py index e951816..2a5e4e0 100644 --- a/scripts/get_jwt.py +++ b/scripts/get_jwt.py @@ -8,7 +8,7 @@ def simulate_login(username): response.raise_for_status() # Raise an exception for any HTTP errors token_data = response.json() - token = token_data['jwt'] + token = token_data["jwt"] return token except requests.exceptions.RequestException as e: diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 14153d0..4c3f198 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -239,6 +239,211 @@ paths: '500': $ref: '#/components/responses/InternalServerError' # -------------------------------------------- + /bids/{bid_id}/questions: + # -------------------------------------------- + get: + tags: + - questions + summary: Returns all questions for a bid + description: A JSON with item count and array of all questions for a bid + operationId: get_questions + parameters: + - name: bid_id + in: path + description: ID of bid to return questions for + required: true + schema: + type: string + format: uuid + security: + - BearerAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + total_count: + type: integer + example: 1 + items: + type: array + items: + $ref: '#/components/schemas/Question' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + # -------------------------------------------- + post: + tags: + - questions + summary: Create a new question for a bid + description: Create a new question for a bid + operationId: post_question + parameters: + - name: bid_id + in: path + description: ID of bid to add question to + required: true + schema: + type: string + format: uuid + requestBody: + $ref: '#/components/requestBodies/PostQuestion' + required: true + security: + - BearerAuth: [] + responses: + '201': + description: Created + content: + application/json: + schema: + type: object + properties: + total_count: + type: integer + example: 1 + items: + type: array + items: + $ref: '#/components/schemas/Question' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' +# -------------------------------------------- + /bids/{bid_id}/questions/{question_id}: + get: + tags: + - questions + summary: Returns a single question for a bid + description: Returns a single question for a bid + operationId: get_question + parameters: + - name: bid_id + in: path + description: ID of bid to return question for + required: true + schema: + type: string + format: uuid + - name: question_id + in: path + description: ID of question to return + required: true + schema: + type: string + format: uuid + security: + - BearerAuth: [] + responses: + '200': + description: A single question + content: + application/json: + schema: + $ref: '#/components/schemas/Question' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + # -------------------------------------------- + put: + tags: + - questions + summary: Update an existing question for a bid + description: Update an existing question for a bid + operationId: update_question + parameters: + - name: bid_id + in: path + description: ID of bid to update question for + required: true + schema: + type: string + format: uuid + - name: question_id + in: path + description: ID of question to update + required: true + schema: + type: string + format: uuid + requestBody: + $ref: '#/components/requestBodies/PostQuestion' + required: true + security: + - BearerAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Question' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '500': + $ref: '#/components/responses/InternalServerError' + # -------------------------------------------- + delete: + tags: + - questions + summary: Hard delete a question for a bid + description: Hard delete a question for a bid + operationId: delete_question + parameters: + - name: bid_id + in: path + description: ID of bid to delete question for + required: true + schema: + type: string + format: uuid + - name: question_id + in: path + description: ID of question to delete + required: true + schema: + type: string + format: uuid + security: + - BearerAuth: [] + responses: + # return 204 (No Content) + '204': + description: Question deleted + content: + noContent: {} + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + # -------------------------------------------- # Components components: @@ -253,9 +458,6 @@ components: - tender - client - bid_date - - status - - links - - last_updated properties: tender: type: string @@ -356,6 +558,71 @@ components: description: Summary of feedback type: string example: 'Feedback from client in detail' + +# -------------------------------------------- + BidLink: + description: A link to a bid + type: string + example: 'https://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428' +# -------------------------------------------- + Question: + description: Question for a bid + type: object + required: + - _id + - description + - question_url + - feedback + - last_updated + - links + properties: + _id: + type: string + format: uuid + example: "471fea1f-705c-4851-9a5b-df7bc2651428" + description: + description: Question + type: string + example: 'THIS IS A QUESTION' + response: + description: Answer + type: string + example: 'THIS IS AN ANSWER' + question_url: + description: Link to question + type: string + example: 'https://organisation.sharepoint.com/Docs/dummyfolder/question' + score: + description: Score achieved at phase + type: string + example: "10" + out_of: + description: Maximum score + type: string + example: "20" + feedback: + type: object + $ref: '#/components/schemas/Feedback' + respondents: + type: array + items: + type: string + example: 'ONS' + last_updated: + type: string + example: "2023-06-27T14:05:17.623827" + links: + type: object + required: + - self + properties: + self: + $ref: '#/components/schemas/SelfQuestionLink' + bid: + $ref: '#/components/schemas/SelfLink' + + + # -------------------------------------------- QuestionsLink: description: A link to a collection of questions for a bid @@ -365,7 +632,13 @@ components: SelfLink: description: A link to the current resource type: string - example: 'https://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428' + example: "https://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428" +# -------------------------------------------- + SelfQuestionLink: + description: A link to the current resource + type: string + example: "https://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions/471fea1f-705c-4851-9a5b-df7bc2651428" + # -------------------------------------------- BidRequestBody: type: object @@ -402,6 +675,36 @@ components: type: array items: $ref: '#/components/schemas/Phase' + + QuestionRequestBody: + type: object + required: + - description + - feedback + properties: + description: + description: Question + type: string + response: + description: Answer + type: string + question_url: + description: Link to question + type: string + score: + description: Score achieved at phase + type: integer + out_of: + description: Maximum score + type: integer + feedback: + type: object + $ref: '#/components/schemas/Feedback' + respondents: + type: string + example: 'ONS' +# -------------------------------------------- +# Security schemes securitySchemes: ApiKeyAuth: type: apiKey @@ -505,6 +808,34 @@ components: summary: 200 OK value: status: 'completed' + PostQuestion: + description: Question object to be added to collection + content: + application/json: + schema: + $ref: '#/components/schemas/QuestionRequestBody' + examples: + 200 OK: + summary: 200 OK + value: + description: 'THIS IS A QUESTION' + response: 'THIS IS AN ANSWER' + question_url: 'https://organisation.sharepoint.com/Docs/dummyfolder/question' + score: 10 + out_of: 20 + feedback: + url: 'https://organisation.sharepoint.com/Docs/dummyfolder/feedback' + description: 'Feedback from client in detail' + respondents: 'ONS' + 400 Bad Request: + summary: 400 Bad Request + value: + description: 'THIS IS A QUESTION' + response: 'THIS IS AN ANSWER' + score: 10 + out_of: 20 + respondents: 'ONS' + # -------------------------------------------- # Error responses responses: From b8662ac30e412ad79b6e1905db64dfd248fab958 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 1 Aug 2023 15:30:42 +0100 Subject: [PATCH 132/208] fix: mandatory field added --- static/swagger_config.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 4c3f198..51b42a1 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -575,6 +575,7 @@ components: - feedback - last_updated - links + - status properties: _id: type: string @@ -603,6 +604,14 @@ components: feedback: type: object $ref: '#/components/schemas/Feedback' + status: + type: string + description: Bid Status + example: in_progress + enum: + - in_progress + - deleted + - completed respondents: type: array items: @@ -681,6 +690,7 @@ components: required: - description - feedback + - question_url properties: description: description: Question From 0567316e8926b3787c490a6cc474eca702379074 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 1 Aug 2023 16:14:33 +0100 Subject: [PATCH 133/208] feat: renamed script files; created questions dummy data --- .../{create_sample_data.py => create_bids.py} | 2 +- scripts/create_questions.py | 0 scripts/{delete_db.py => delete_bids.py} | 2 +- scripts/test_data/questions.json | 98 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) rename scripts/{create_sample_data.py => create_bids.py} (96%) create mode 100644 scripts/create_questions.py rename scripts/{delete_db.py => delete_bids.py} (93%) create mode 100644 scripts/test_data/questions.json diff --git a/scripts/create_sample_data.py b/scripts/create_bids.py similarity index 96% rename from scripts/create_sample_data.py rename to scripts/create_bids.py index 3b1fcb6..9f43bbd 100644 --- a/scripts/create_sample_data.py +++ b/scripts/create_bids.py @@ -1,6 +1,6 @@ """ -This script creates sample data for the MongoDB database. +This script creates sample data for the Bids collection. """ diff --git a/scripts/create_questions.py b/scripts/create_questions.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/delete_db.py b/scripts/delete_bids.py similarity index 93% rename from scripts/delete_db.py rename to scripts/delete_bids.py index a36efa0..3846798 100644 --- a/scripts/delete_db.py +++ b/scripts/delete_bids.py @@ -1,5 +1,5 @@ """ -This script deletes all bids from the MongoDB collection. +This script deletes all bids from the Bids collection. """ diff --git a/scripts/test_data/questions.json b/scripts/test_data/questions.json new file mode 100644 index 0000000..e1ebd39 --- /dev/null +++ b/scripts/test_data/questions.json @@ -0,0 +1,98 @@ +[ + { + "_id": "6e7d3f8a-fab3-4ebf-8348-96d0808d325e", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder" + }, + "last_updated": "2023-08-01T15:08:33.187216", + "links": { + "bid": "", + "self": "/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": ["JohnDoe", "JaneSmith"], + "response": "This is a sample response.", + "score": 3, + "status": "in_progress" + }, + { + "_id": "c9f6d25b-7d36-4cf3-89e7-1df2f1ca4e21", + "description": "Another question", + "feedback": { + "description": "Excellent feedback", + "url": "https://organisation.sharepoint.com/Docs/anotherfolder" + }, + "last_updated": "2023-08-01T10:20:15.123456", + "links": { + "bid": "", + "self": "/questions/c9f6d25b-7d36-4cf3-89e7-1df2f1ca4e21" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/anotherfolder", + "respondents": ["JohnDoe", "JaneSmith", "AlexBrown"], + "response": "This is another response.", + "score": 2, + "status": "in_progress" + }, + { + "_id": "f9d55b4f-8b34-434b-b768-c6b9b1e9e2c6", + "description": "Third question", + "feedback": { + "description": "Average feedback", + "url": "https://organisation.sharepoint.com/Docs/thirdfolder" + }, + "last_updated": "2023-08-01T12:45:55.987654", + "links": { + "bid": "", + "self": "/questions/f9d55b4f-8b34-434b-b768-c6b9b1e9e2c6" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/thirdfolder", + "respondents": ["AlexBrown"], + "response": "This is the third response.", + "score": 2, + "status": "in_progress" + }, + { + "_id": "1b4c0c2e-2e6f-41d1-836c-43eb32c2d2b1", + "description": "Fourth question", + "feedback": { + "description": "Negative feedback", + "url": "https://organisation.sharepoint.com/Docs/fourthfolder" + }, + "last_updated": "2023-08-01T14:30:40.654321", + "links": { + "bid": "", + "self": "/questions/1b4c0c2e-2e6f-41d1-836c-43eb32c2d2b1" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/fourthfolder", + "respondents": ["JohnDoe"], + "response": "This is a negative response.", + "score": 1, + "status": "in_progress" + }, + { + "_id": "f1b4e0a9-9e4c-4726-9a53-1a64ef684828", + "description": "Fifth question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/fifthfolder" + }, + "last_updated": "2023-08-01T16:15:22.345678", + "links": { + "bid": "", + "self": "/questions/f1b4e0a9-9e4c-4726-9a53-1a64ef684828" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/fifthfolder", + "respondents": ["JaneSmith", "AlexBrown"], + "response": "This is the fifth response.", + "score": 3, + "status": "in_progress" + } + ] + \ No newline at end of file From b41335e84c078b7f49f494387f4a14888e3f09a5 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 2 Aug 2023 09:54:51 +0100 Subject: [PATCH 134/208] feafeat: get all and get by id for the questions --- Makefile | 2 +- api/controllers/question_controller.py | 52 ++++++++++++++++++++++++++ request_examples/get_all_question.http | 2 + request_examples/post_bid.http | 2 +- request_examples/post_question.http | 4 +- static/swagger_config.yml | 6 +-- 6 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 request_examples/get_all_question.http diff --git a/Makefile b/Makefile index 62033a7..5c3a13b 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ help: @echo "gmake test - run the tests" authserver: - $(PYTHON) ../tdse-accessForce-auth-api/app.py + $(PYTHON) ../tdse-accessForce-auth-stub/app.py authplay: @echo "Getting JWT..." diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 9d11fdb..2803ef8 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -38,3 +38,55 @@ def post_question(bid_id): # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 + + +@question.route("/bids//questions", methods=["GET"]) +@require_jwt +def get_questions(bid_id): + try: + hostname = request.headers.get("host") + bid_id = validate_bid_id_path(bid_id) + hostname = request.headers.get("host") + data = list( + db["questions"].find( + { + "status": {"$ne": Status.DELETED.value}, + "links.bid": f"/bids/{bid_id}", + } + ) + ) + + if data is None: + return showNotFoundError(), 404 + else: + for question in data: + prepend_host_to_links(question, hostname) + + return {"total_count": len(data), "items": data}, 200 + + except ValidationError as e: + return showValidationError(e), 400 + except Exception: + return showInternalServerError(), 500 + + +@question.route("/bids//questions/", methods=["GET"]) +@require_jwt +def get_question(bid_id, question_id): + try: + bid_id = validate_bid_id_path(bid_id) + question_id = validate_bid_id_path(question_id) + hostname = request.headers.get("host") + data = db["questions"].find_one( + {"_id": question_id, "links.bid": f"/bids/{bid_id}"} + ) + + if data is None: + return showNotFoundError(), 404 + else: + prepend_host_to_links(data, hostname) + return data, 200 + except ValidationError as e: + return showValidationError(e), 400 + except Exception as e: + return showInternalServerError(), 500 diff --git a/request_examples/get_all_question.http b/request_examples/get_all_question.http new file mode 100644 index 0000000..98b404a --- /dev/null +++ b/request_examples/get_all_question.http @@ -0,0 +1,2 @@ +GET http://localhost:8080/api/bids/66fb5dba-f129-413a-b12e-5a68b5a647d6/questions/d3df9364-0640-4ac2-b826-0de07636833e HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU3MDIyNn0.93o-R3VGeNHAbfthiJ4odsA8n9eBu7ZKwU2KP06EE6E \ No newline at end of file diff --git a/request_examples/post_bid.http b/request_examples/post_bid.http index b19effa..8b561e5 100644 --- a/request_examples/post_bid.http +++ b/request_examples/post_bid.http @@ -1,6 +1,6 @@ POST http://localhost:8080/api/bids HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJhIiwiYWRtaW4iOmZhbHNlLCJleHAiOjE2OTE0MjAxODZ9.FuanolVVEPL32xDTJfnqj7w5GEOoGI5qNu2nILmxTfM +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTUyNDk0M30.GYWsLyCddSJqxFBCXJc5OMivZsQNQBUTaW6rd0bfq7A { "tender": "Business Intelligence and Data Warehousing", diff --git a/request_examples/post_question.http b/request_examples/post_question.http index 8778aa7..7699907 100644 --- a/request_examples/post_question.http +++ b/request_examples/post_question.http @@ -1,6 +1,6 @@ -POST http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions HTTP/1.1 +POST http://localhost:8080/api/bids/66fb5dba-f129-413a-b12e-5a68b5a647d6/questions HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJhIiwiYWRtaW4iOmZhbHNlLCJleHAiOjE2OTE0MjAxODZ9.FuanolVVEPL32xDTJfnqj7w5GEOoGI5qNu2nILmxTfM +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU3MDIyNn0.93o-R3VGeNHAbfthiJ4odsA8n9eBu7ZKwU2KP06EE6E { "description": "This is a question", diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 51b42a1..7d5c7d1 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -636,17 +636,17 @@ components: QuestionsLink: description: A link to a collection of questions for a bid type: string - example: 'https://{hostname}/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions' + example: 'http://{hostname}/api//bids/96d69775-29af-46b1-aaf4-bfbdb1543412/questions' # -------------------------------------------- SelfLink: description: A link to the current resource type: string - example: "https://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428" + example: 'http://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428' # -------------------------------------------- SelfQuestionLink: description: A link to the current resource type: string - example: "https://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions/471fea1f-705c-4851-9a5b-df7bc2651428" + example: 'http://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions/471fea1f-705c-4851-9a5b-df7bc2651428' # -------------------------------------------- BidRequestBody: From 800fb834f8b19c3c0338f3e3ff54dd3361e0a451 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 2 Aug 2023 12:04:14 +0100 Subject: [PATCH 135/208] feat: completed questions script; added make targets --- Makefile | 15 ++++-- scripts/create_bids.py | 4 +- scripts/create_questions.py | 84 ++++++++++++++++++++++++++++++++ scripts/delete_bids.py | 11 ++--- scripts/delete_questions.py | 44 +++++++++++++++++ scripts/test_data/questions.json | 77 +++++++++++++++++++++++++++++ 6 files changed, 222 insertions(+), 13 deletions(-) create mode 100644 scripts/delete_questions.py diff --git a/Makefile b/Makefile index 62033a7..61f7804 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,8 @@ authplay: @find . -name "get_jwt.py" -exec python3 {} \; bids: - @echo "Creating sample data..." - @find . -name "create_sample_data.py" -exec python3 {} \; + @echo "Creating bids..." + @find . -name "create_bids.py" -exec python3 {} \; branch: @echo "Available branch types:" @@ -71,7 +71,8 @@ commit: format dbclean: @echo "Cleaning up database..." - @find . -name "delete_db.py" -exec python3 {} \; + @find . -name "delete_bids.py" -exec python3 {} \; + @find . -name "delete_questions.py" -exec python3 {} \; format: $(PYTHON) -m black . @@ -91,8 +92,8 @@ mongostop: run: build $(PYTHON) app.py -setup: build dbclean bids - @echo "Setting up the application database..." +setup: dbclean bids questions + @echo "Database setup complete." swag: open http://localhost:8080/api/docs/#/ @@ -102,6 +103,10 @@ test: @echo "TEST COVERAGE REPORT" coverage report -m --omit="tests/*,dbconfig/*" +questions: + @echo "Creating questions..." + @find . -name "create_questions.py" -exec python3 {} \; + venv/bin/activate: requirements.txt python3 -m venv .venv $(PIP) install -r requirements.txt diff --git a/scripts/create_bids.py b/scripts/create_bids.py index 9f43bbd..1beca63 100644 --- a/scripts/create_bids.py +++ b/scripts/create_bids.py @@ -45,8 +45,8 @@ def populate_bids(): collection.insert_one(bid) print(f"Inserted bid with _id: {bid['_id']}") - except ConnectionFailure as error: - print(f"Error: {error}") + except ConnectionFailure: + print(f"Error: Failed to connect to database") sys.exit(1) finally: diff --git a/scripts/create_questions.py b/scripts/create_questions.py index e69de29..1f5f096 100644 --- a/scripts/create_questions.py +++ b/scripts/create_questions.py @@ -0,0 +1,84 @@ +""" + +This script creates sample data for the Questions collection. + +""" + +import os +import json +import sys +from itertools import zip_longest +from pymongo import MongoClient +from pymongo.errors import ConnectionFailure +from dotenv import load_dotenv + +load_dotenv() + +MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" + + +def populate_questions(): + """ + Populates the MongoDB database with sample questions data from questions.json file. + """ + try: + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) + data_base = client["bidsAPI"] + collection = data_base["questions"] + + # Get the current script's directory + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # Construct the file path to bids.json + bids_path = os.path.join(current_dir, "test_data", "bids.json") + + # Construct the file path to questions.json + questions_path = os.path.join(current_dir, "test_data", "questions.json") + + # Read bids data from JSON file + with open(bids_path, encoding="utf-8") as bids_file: + bids_data = json.load(bids_file) + + # Read bids data from JSON file + with open(questions_path, encoding="utf-8") as questions_file: + questions_data = json.load(questions_file) + + # Update questions data with existing bid ids from bids.json + updated_questions = [] + + bids = bids_data["items"] + questions_groups = [ + questions_data[i : i + 3] for i in range(0, len(questions_data), 3) + ] + + for bid, questions in zip_longest(bids, questions_groups, fillvalue=None): + bid_url = bid["links"]["self"] + + for question in questions: + self_url = question["links"]["self"] + question["links"]["bid"] = bid_url + question["links"]["self"] = f"{bid_url}{self_url}" + updated_questions.append(question) + + # Insert questions into the database + for question in updated_questions: + # Check if the question already exists in the database + existing_question = collection.find_one({"_id": question["_id"]}) + if existing_question: + print(f"Skipping existing question with _id: {question['_id']}") + else: + collection.insert_one(question) + print(f"Inserted question with _id: {question['_id']}") + + except ConnectionFailure: + print(f"Error: Failed to connect to database") + sys.exit(1) + + finally: + # Close the MongoDB connection + client.close() + + +if __name__ == "__main__": + populate_questions() + sys.exit(0) diff --git a/scripts/delete_bids.py b/scripts/delete_bids.py index 3846798..128fc19 100644 --- a/scripts/delete_bids.py +++ b/scripts/delete_bids.py @@ -25,13 +25,12 @@ def delete_bids(): if collection.count_documents({}) == 0: print("No bids to delete.") + else: + delete_result = collection.delete_many({}) + print(f"Deleted {delete_result.deleted_count} bids from the collection.") - delete_result = collection.delete_many({}) - - print(f"Deleted {delete_result.deleted_count} bids from the collection.") - - except ConnectionFailure as error: - print(f"Error: {error}") + except ConnectionFailure: + print(f"Error: Failed to connect to database") sys.exit(1) finally: diff --git a/scripts/delete_questions.py b/scripts/delete_questions.py new file mode 100644 index 0000000..bfc662a --- /dev/null +++ b/scripts/delete_questions.py @@ -0,0 +1,44 @@ +""" +This script deletes all questions from the Questions collection. + +""" + +import os +import sys +from pymongo import MongoClient +from pymongo.errors import ConnectionFailure +from dotenv import load_dotenv + +load_dotenv() + +MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" + + +def delete_bids(): + """ + Deletes all bids from the MongoDB collection. + """ + try: + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) + data_base = client["bidsAPI"] + collection = data_base["questions"] + + if collection.count_documents({}) == 0: + print("No questions to delete.") + else: + delete_result = collection.delete_many({}) + print( + f"Deleted {delete_result.deleted_count} questions from the collection." + ) + + except ConnectionFailure: + print(f"Error: Failed to connect to database") + sys.exit(1) + + finally: + client.close() + + +if __name__ == "__main__": + delete_bids() + sys.exit(0) diff --git a/scripts/test_data/questions.json b/scripts/test_data/questions.json index e1ebd39..262428d 100644 --- a/scripts/test_data/questions.json +++ b/scripts/test_data/questions.json @@ -93,6 +93,83 @@ "response": "This is the fifth response.", "score": 3, "status": "in_progress" + }, + { + "_id": "67c6b84d-3e8f-4c0e-b4c3-2e46e42e2d2d", + "description": "Sixth question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/sixthfolder" + }, + "last_updated": "2023-08-02T10:15:10.987654", + "links": { + "bid": "", + "self": "/questions/67c6b84d-3e8f-4c0e-b4c3-2e46e42e2d2d" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/sixthfolder", + "respondents": ["JohnDoe"], + "response": "This is the sixth response.", + "score": 2, + "status": "in_progress" + }, + { + "_id": "15f13a6e-81d1-49c1-94d2-82bc79a1e968", + "description": "Seventh question", + "feedback": { + "description": "Average feedback", + "url": "https://organisation.sharepoint.com/Docs/seventhfolder" + }, + "last_updated": "2023-08-02T12:30:25.567890", + "links": { + "bid": "", + "self": "/questions/15f13a6e-81d1-49c1-94d2-82bc79a1e968" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/seventhfolder", + "respondents": ["JaneSmith", "AlexBrown"], + "response": "This is the seventh response.", + "score": 2, + "status": "in_progress" + }, + { + "_id": "3a45123c-24a1-4d7f-b792-45d9dd29fc29", + "description": "Eighth question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/eighthfolder" + }, + "last_updated": "2023-08-03T09:45:18.111222", + "links": { + "bid": "", + "self": "/questions/3a45123c-24a1-4d7f-b792-45d9dd29fc29" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/eighthfolder", + "respondents": ["JohnDoe", "JaneSmith", "AlexBrown"], + "response": "This is the eighth response.", + "score": 3, + "status": "in_progress" + }, + { + "_id": "b259a93e-7b08-4a80-8daa-8cbb5beebcd9", + "description": "Ninth question", + "feedback": { + "description": "Negative feedback", + "url": "https://organisation.sharepoint.com/Docs/ninthfolder" + }, + "last_updated": "2023-08-03T11:20:30.222333", + "links": { + "bid": "", + "self": "/questions/b259a93e-7b08-4a80-8daa-8cbb5beebcd9" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/ninthfolder", + "respondents": ["JohnDoe"], + "response": "This is a negative response.", + "score": 1, + "status": "in_progress" } ] + \ No newline at end of file From 2b231ae464f4c04e58e68ba467fb07b2b16ec488 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 2 Aug 2023 12:08:53 +0100 Subject: [PATCH 136/208] refactor: updated make target authserver for renamed auth-stub --- Makefile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 61f7804..4058107 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,13 @@ PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: run test clean check help commit swagger format branch lint setup bids dbclean authserver authplay +.PHONY: run test clean check help commit swagger format branch lint setup bids questions dbclean authserver authplay help: @echo "gmake help - display this help" + @echo "gmake authplay - get JWT and interact with auth api" @echo "gmake authserver - run auth api application" - @echo "gmake auth - get JWT and interact with auth api" - @echo "gmake bids - create sample data" + @echo "gmake bids - populate bids collection" @echo "gmake branch - create a new branch" @echo "gmake build - create and activate virtual environment" @echo "gmake check - check for security vulnerabilities" @@ -27,9 +27,10 @@ help: @echo "gmake swagger - open swagger documentation" @echo "gmake setup - setup the application database" @echo "gmake test - run the tests" + @echo "gmake questions - populate questions collection" authserver: - $(PYTHON) ../tdse-accessForce-auth-api/app.py + $(PYTHON) ../tdse-accessForce-auth-stub/app.py authplay: @echo "Getting JWT..." From a2b38e1d11101b5a0f58a4752c8d955cd617b20b Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 2 Aug 2023 12:50:10 +0100 Subject: [PATCH 137/208] refactor: added make targets to help --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 4058107..83b7bb0 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,8 @@ help: @echo "gmake dbclean - clean up the application database" @echo "gmake format - format the code" @echo "gmake lint - run linters" + @echo "gmake mongostart - run local mongodb instance" + @echo "gmake mongostop - stop local mongodb instance" @echo "gmake run - run the application" @echo "gmake swagger - open swagger documentation" @echo "gmake setup - setup the application database" From 955e3a3a1a3cec3797f73495c6b9d583d89afb69 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 2 Aug 2023 12:51:01 +0100 Subject: [PATCH 138/208] =?UTF-8?q?test:=20get=20question=2066%=20done=1B[?= =?UTF-8?q?D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- request_examples/get_all_question.http | 2 +- tests/test_get_questions.py | 119 +++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 tests/test_get_questions.py diff --git a/request_examples/get_all_question.http b/request_examples/get_all_question.http index 98b404a..d2d7d32 100644 --- a/request_examples/get_all_question.http +++ b/request_examples/get_all_question.http @@ -1,2 +1,2 @@ -GET http://localhost:8080/api/bids/66fb5dba-f129-413a-b12e-5a68b5a647d6/questions/d3df9364-0640-4ac2-b826-0de07636833e HTTP/1.1 +GET http://localhost:8080/api/bids/66fb5dba-f129-413a-b12e-5a68b5a647d6/questions HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU3MDIyNn0.93o-R3VGeNHAbfthiJ4odsA8n9eBu7ZKwU2KP06EE6E \ No newline at end of file diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py new file mode 100644 index 0000000..36ec0b4 --- /dev/null +++ b/tests/test_get_questions.py @@ -0,0 +1,119 @@ +from unittest.mock import patch + + +# Case 1: Successful get +@patch("api.controllers.question_controller.db") +def test_get_questions_success(mock_db, test_client, basic_jwt): + # Set up the sample data and expected result + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_data = [ + { + "_id": "2b18f477-627f-4d48-a008-ca0d9cea3791", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/bids/{sample_bid_id}", + "self": f"/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + }, + { + "_id": "bef5c1fb-46b4-4707-868a-c7719cfcc5ec", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-02T09:35:58.295052", + "links": { + "bid": f"/bids/another-bid-id", + "self": f"/bids/another-bid-id/questions/bef5c1fb-46b4-4707-868a-c7719cfcc5ec", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + }, + ] + + # Filter the sample data to include only questions with the desired bid link + filtered_sample_data = [ + question + for question in sample_data + if question["links"]["bid"] == f"/bids/{sample_bid_id}" + ] + + # Mock the database find method to return the filtered sample data + mock_db["questions"].find.return_value = filtered_sample_data + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + # Assert the response status code and content + assert response.status_code == 200 + response_data = response.get_json() + assert response_data["total_count"] == len(filtered_sample_data) + assert response_data["items"] == filtered_sample_data + + +# # Case 2: Links prepended with hostname +@patch("api.controllers.question_controller.db") +def test_links_with_host(mock_db, test_client, basic_jwt): + # Set up the sample data and expected result + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_data = [ + { + "_id": "2b18f477-627f-4d48-a008-ca0d9cea3791", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/bids/{sample_bid_id}", + "self": f"/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + } + ] + + # Mock the database find method to return the filtered sample data + mock_db["questions"].find.return_value = sample_data + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + # Assert the response status code and content + response_data = response.get_json() + assert ( + response_data["items"][0]["links"]["bid"] + == f"http://localhost:8080/bids/{sample_bid_id}" + ) + + assert ( + response_data["items"][0]["links"]["self"] + == f"http://localhost:8080/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791" + ) From 22163e1736f0157ef1ba504f10551c578a079787 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 2 Aug 2023 13:00:49 +0100 Subject: [PATCH 139/208] docs: update scripts readme --- scripts/README.md | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index 4dfb52e..b904001 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -3,32 +3,49 @@ ## Database Cleanup Script ### Script Description -- The delete_db.py script is used to delete all bids from the MongoDB collection. -- The create_sample_data.py script is used to populate the MongoDB database with sample bids data from the bids.json file. +- The delete_bids.py script is used to delete all bids from the MongoDB collection. +- The delete_questions.py script is used to delete all questions from the MongoDB collection. +- The create_bids.py script is used to populate the MongoDB database with sample bids data from the bids.json file. +- The create_questions.py script is used to populate the MongoDB database with sample questions data from the questions.json file, using existing bid Ids from the bids.json file. ### Usage To run the database cleanup script, execute the following command: ```bash -python3 delete_db.py +gmake dbclean ``` -To run the sample data population script, execute the following command: +Or to run the cleanup script for only the bids collection, execute: ```bash -python3 create_sample_data.py +python3 delete_bids.py ``` -## Application Setup +And to run the cleanup script for only the questions collection, execute: +```bash +python3 delete_questions.py +``` + +To run the sample bids data population script, execute the following command: +```bash +python3 create_bids.py +``` + +To run the sample questions data population script, execute the following command: +```bash +python3 create_questions.py +``` + +## Database Setup ### Script Description The setup target in the Makefile sets up the application database by performing the following steps: -1. Building the application using the build target. -2. Cleaning up the existing database using the dbclean target. -3. Creating sample data using the bids target. +1. Cleaning up the existing database using the dbclean target. +2. Creating sample data using the bids target. +3. Creating sample data using the questions target. ### Usage To set up the application database, run the following command: ```bash -make setup +gmake setup ``` \ No newline at end of file From 90684cbf7269409d623af956474dab91c3971666 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 2 Aug 2023 14:24:52 +0100 Subject: [PATCH 140/208] refactor: added check if bid exists and error handler in post question; updated helper validate_bid_id to validate_id for reusability; updated tests to reflect changes --- api/controllers/bid_controller.py | 10 ++++---- api/controllers/question_controller.py | 33 ++++++++++++++++++-------- api/schemas/bid_id_schema.py | 14 ----------- api/schemas/id_schema.py | 14 +++++++++++ helpers/helpers.py | 8 +++---- request_examples/get_all_question.http | 2 +- tests/test_delete_bid.py | 2 +- tests/test_get_bid_by_id.py | 2 +- 8 files changed, 49 insertions(+), 36 deletions(-) delete mode 100644 api/schemas/bid_id_schema.py create mode 100644 api/schemas/id_schema.py diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index e700c76..1583b35 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -10,7 +10,7 @@ showUnprocessableEntityError, showValidationError, validate_and_create_bid_document, - validate_bid_id_path, + validate_id_path, validate_bid_update, validate_status_update, prepend_host_to_links, @@ -57,7 +57,7 @@ def post_bid(): @require_api_key def get_bid_by_id(bid_id): try: - bid_id = validate_bid_id_path(bid_id) + bid_id = validate_id_path(bid_id) data = db["bids"].find_one( {"_id": bid_id, "status": {"$ne": Status.DELETED.value}} ) @@ -81,7 +81,7 @@ def get_bid_by_id(bid_id): @require_jwt def update_bid_by_id(bid_id): try: - bid_id = validate_bid_id_path(bid_id) + bid_id = validate_id_path(bid_id) # Retrieve resource where id is equal to bid_id current_bid = db["bids"].find_one( {"_id": bid_id, "status": Status.IN_PROGRESS.value} @@ -109,7 +109,7 @@ def update_bid_by_id(bid_id): @require_admin_access def change_status_to_deleted(bid_id): try: - bid_id = validate_bid_id_path(bid_id) + bid_id = validate_id_path(bid_id) data = db["bids"].find_one_and_update( {"_id": bid_id, "status": {"$ne": Status.DELETED.value}}, { @@ -134,7 +134,7 @@ def change_status_to_deleted(bid_id): @require_admin_access def update_bid_status(bid_id): try: - bid_id = validate_bid_id_path(bid_id) + bid_id = validate_id_path(bid_id) # 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 diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 2803ef8..d3d9dec 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -10,7 +10,7 @@ showUnprocessableEntityError, showValidationError, validate_and_create_question_document, - validate_bid_id_path, + validate_id_path, validate_bid_update, validate_status_update, prepend_host_to_links, @@ -26,7 +26,11 @@ @require_jwt def post_question(bid_id): try: - bid_id = validate_bid_id_path(bid_id) + bid_id = validate_id_path(bid_id) + # Check if the bid exists in the database + bid = db["bids"].find_one({"_id": bid_id}) + if not bid: + return showNotFoundError(), 404 # Process input and create data model data = validate_and_create_question_document(request.get_json(), bid_id) # Insert document into database collection @@ -44,8 +48,7 @@ def post_question(bid_id): @require_jwt def get_questions(bid_id): try: - hostname = request.headers.get("host") - bid_id = validate_bid_id_path(bid_id) + bid_id = validate_id_path(bid_id) hostname = request.headers.get("host") data = list( db["questions"].find( @@ -55,15 +58,12 @@ def get_questions(bid_id): } ) ) - if data is None: return showNotFoundError(), 404 else: for question in data: prepend_host_to_links(question, hostname) - return {"total_count": len(data), "items": data}, 200 - except ValidationError as e: return showValidationError(e), 400 except Exception: @@ -74,13 +74,12 @@ def get_questions(bid_id): @require_jwt def get_question(bid_id, question_id): try: - bid_id = validate_bid_id_path(bid_id) - question_id = validate_bid_id_path(question_id) + bid_id = validate_id_path(bid_id) + question_id = validate_id_path(question_id) hostname = request.headers.get("host") data = db["questions"].find_one( {"_id": question_id, "links.bid": f"/bids/{bid_id}"} ) - if data is None: return showNotFoundError(), 404 else: @@ -90,3 +89,17 @@ def get_question(bid_id, question_id): return showValidationError(e), 400 except Exception as e: return showInternalServerError(), 500 + + +@question.route("/bids//questions/", methods=["DELETE"]) +@require_admin_access +def delete_question(bid_id, question_id): + try: + bid_id = validate_id_path(bid_id) + question_id = validate_id_path(question_id) + data = db["questions"].delete_one({"_id": question_id}) + return data, 204 + except ValidationError as e: + return showValidationError(e), 400 + except Exception: + return showInternalServerError(), 500 diff --git a/api/schemas/bid_id_schema.py b/api/schemas/bid_id_schema.py deleted file mode 100644 index 291e698..0000000 --- a/api/schemas/bid_id_schema.py +++ /dev/null @@ -1,14 +0,0 @@ -from marshmallow import Schema, fields, validate - - -class BidIdSchema(Schema): - """ - Schema for validating bid IDs. - - Attributes: - bid_id (str): The bid ID to be validated. - """ - - bid_id = fields.Str( - required=True, validate=validate.Length(min=36, error="Invalid bid Id") - ) diff --git a/api/schemas/id_schema.py b/api/schemas/id_schema.py new file mode 100644 index 0000000..98f9eab --- /dev/null +++ b/api/schemas/id_schema.py @@ -0,0 +1,14 @@ +from marshmallow import Schema, fields, validate + + +class IdSchema(Schema): + """ + Schema for validating path param IDs. + + Attributes: + id (str): The bid ID to be validated. + """ + + id = fields.Str( + required=True, validate=validate.Length(equal=36, error="Invalid Id") + ) diff --git a/helpers/helpers.py b/helpers/helpers.py index 865b659..e71ce82 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -8,7 +8,7 @@ from functools import wraps from werkzeug.exceptions import UnprocessableEntity from api.schemas.bid_schema import BidSchema -from api.schemas.bid_id_schema import BidIdSchema +from api.schemas.id_schema import IdSchema from api.schemas.question_schema import QuestionSchema @@ -60,9 +60,9 @@ def validate_and_create_bid_document(request): return data -def validate_bid_id_path(bid_id): - valid_bid_id = BidIdSchema().load({"bid_id": bid_id}) - data = valid_bid_id["bid_id"] +def validate_id_path(id): + valid_id = IdSchema().load({"id": id}) + data = valid_id["id"] return data diff --git a/request_examples/get_all_question.http b/request_examples/get_all_question.http index d2d7d32..a04fc34 100644 --- a/request_examples/get_all_question.http +++ b/request_examples/get_all_question.http @@ -1,2 +1,2 @@ -GET http://localhost:8080/api/bids/66fb5dba-f129-413a-b12e-5a68b5a647d6/questions HTTP/1.1 +GET http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU3MDIyNn0.93o-R3VGeNHAbfthiJ4odsA8n9eBu7ZKwU2KP06EE6E \ No newline at end of file diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index c3233e5..8442537 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -35,7 +35,7 @@ def test_delete_bid_validation_error(mock_db, test_client, admin_jwt): "/api/bids/invalid_bid_id", headers={"Authorization": f"Bearer {admin_jwt}"} ) assert response.status_code == 400 - assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} + assert response.get_json() == {"Error": "{'id': ['Invalid Id']}"} # Case 4: Bid not found diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index a236e74..71b7b81 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -81,4 +81,4 @@ def test_get_bid_by_id_validation_error(mock_db, test_client, api_key): headers={"host": "localhost:8080", "X-API-Key": api_key}, ) assert response.status_code == 400 - assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} + assert response.get_json() == {"Error": "{'id': ['Invalid Id']}"} From 0ad60e3dfe79a9f98e6b540ec13ade7fbf8705a2 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 2 Aug 2023 15:09:19 +0100 Subject: [PATCH 141/208] test: get all messages from a bid and 1 message by id --- api/controllers/question_controller.py | 21 +-- request_examples/post_question.http | 4 +- tests/test_get_question_by_id.py | 170 +++++++++++++++++++++++++ tests/test_get_questions.py | 75 ++++++++++- 4 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 tests/test_get_question_by_id.py diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 2803ef8..da127f0 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -56,11 +56,11 @@ def get_questions(bid_id): ) ) - if data is None: + if len(data) == 0: return showNotFoundError(), 404 - else: - for question in data: - prepend_host_to_links(question, hostname) + + for question in data: + prepend_host_to_links(question, hostname) return {"total_count": len(data), "items": data}, 200 @@ -78,13 +78,18 @@ def get_question(bid_id, question_id): question_id = validate_bid_id_path(question_id) hostname = request.headers.get("host") data = db["questions"].find_one( - {"_id": question_id, "links.bid": f"/bids/{bid_id}"} + { + "_id": question_id, + "links.bid": f"/bids/{bid_id}", + "status": {"$ne": Status.DELETED.value}, + } ) - if data is None: + if len(data) == 0: return showNotFoundError(), 404 - else: - prepend_host_to_links(data, hostname) + + prepend_host_to_links(data, hostname) + return data, 200 except ValidationError as e: return showValidationError(e), 400 diff --git a/request_examples/post_question.http b/request_examples/post_question.http index 7699907..d0cb27c 100644 --- a/request_examples/post_question.http +++ b/request_examples/post_question.http @@ -1,6 +1,6 @@ -POST http://localhost:8080/api/bids/66fb5dba-f129-413a-b12e-5a68b5a647d6/questions HTTP/1.1 +POST http://localhost:8080/api/bids/66fb5dba-f129-441a-b12e-5a68b5a647d6/questions HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU3MDIyNn0.93o-R3VGeNHAbfthiJ4odsA8n9eBu7ZKwU2KP06EE6E +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e6J1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU3MDIyNn0.93o-R3VGeNHAbfthiJ4odsA8n9eBu7ZKwU2KP06EE6E { "description": "This is a question", diff --git a/tests/test_get_question_by_id.py b/tests/test_get_question_by_id.py new file mode 100644 index 0000000..c65fa72 --- /dev/null +++ b/tests/test_get_question_by_id.py @@ -0,0 +1,170 @@ +from unittest.mock import patch + + +# Case 1: Successful get +@patch("api.controllers.question_controller.db") +def test_get_single_question_success(mock_db, test_client, basic_jwt): + # Set up the sample bid ID and question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + + # Set up the sample data for a single question + sample_data = { + "_id": sample_question_id, + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/bids/{sample_bid_id}", + "self": f"/bids/{sample_bid_id}/questions/{sample_question_id}", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + } + + # Mock the database find_one method to return the sample question + mock_db["questions"].find_one.return_value = sample_data + + # Make a request to the endpoint to get the single question + response = test_client.get( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + # Assert the response status code and content + assert response.status_code == 200 + response_data = response.get_json() + assert response_data == sample_data + + +# Case 2: Links prepended with hostname +@patch("api.controllers.question_controller.db") +def test_single_question_links_with_host(mock_db, test_client, basic_jwt): + # Set up the sample bid ID and question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + + # Set up the sample data for a single question + sample_data = { + "_id": sample_question_id, + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/bids/{sample_bid_id}", + "self": f"/bids/{sample_bid_id}/questions/{sample_question_id}", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + } + + # Mock the database find_one method to return the sample question + mock_db["questions"].find_one.return_value = sample_data + + # Make a request to the endpoint to get the single question with hostname in the headers + response = test_client.get( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + # Assert the response status code and content + assert response.status_code == 200 + response_data = response.get_json() + assert ( + response_data["links"]["bid"] == f"http://localhost:8080/bids/{sample_bid_id}" + ) + assert ( + response_data["links"]["self"] + == f"http://localhost:8080/bids/{sample_bid_id}/questions/{sample_question_id}" + ) + + +# Case 3: Connection error +@patch("api.controllers.question_controller.db") +def test_get_single_question_connection_error(mock_db, test_client, basic_jwt): + # Set up the sample bid ID and question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + + # Mock the database find_one method to raise a ConnectionError + mock_db["questions"].find_one.side_effect = Exception + + # Make a request to the endpoint to get the single question + response = test_client.get( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + # Assert the response status code and content + assert response.status_code == 500 + response_data = response.get_json() + assert response_data == {"Error": "Could not connect to database"} + + +# Case 4: Unauthorized / invalid api key +@patch("api.controllers.question_controller.db") +def test_get_single_question_unauthorized(mock_db, test_client): + # Set up the sample bid ID and question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + + # Mock the database find_one method to return the question + mock_db["questions"].find_one.return_value = {} + + # Make a request to the endpoint to get the single question without providing a JWT + response = test_client.get( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080"}, + ) + + # Assert the response status code and content + assert response.status_code == 401 + response_data = response.get_json() + assert response_data == {"Error": "Unauthorized"} + + # Make a request to the endpoint to get the single question with an invalid JWT + response = test_client.get( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": "Bearer INVALID_JWT"}, + ) + + # Assert the response status code and content + assert response.status_code == 401 + response_data = response.get_json() + assert response_data == {"Error": "Unauthorized"} + + +# Case 5: No question found for the given ID +@patch("api.controllers.question_controller.db") +def test_no_question_found_by_id(mock_db, test_client, basic_jwt): + # Set up the sample question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + + # Mock the database find_one method to return None (no question found) + mock_db["questions"].find_one.return_value = [] + + # Make a request to the endpoint to get the question by ID + response = test_client.get( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + # Assert the response status code and content + assert response.status_code == 404 + response_data = response.get_json() + assert response_data == {"Error": "Resource not found"} diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index 36ec0b4..c449ed3 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -70,7 +70,7 @@ def test_get_questions_success(mock_db, test_client, basic_jwt): assert response_data["items"] == filtered_sample_data -# # Case 2: Links prepended with hostname +# Case 2: Links prepended with hostname @patch("api.controllers.question_controller.db") def test_links_with_host(mock_db, test_client, basic_jwt): # Set up the sample data and expected result @@ -117,3 +117,76 @@ def test_links_with_host(mock_db, test_client, basic_jwt): response_data["items"][0]["links"]["self"] == f"http://localhost:8080/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791" ) + + +# Case 3: Connection error +@patch("api.controllers.question_controller.db") +def test_get_questions_connection_error(mock_db, test_client, basic_jwt): + # Set up the sample bid ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + + # Mock the database find method to raise a ConnectionError + mock_db["questions"].find.side_effect = Exception + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + # Assert the response status code and content + assert response.status_code == 500 + response_data = response.get_json() + assert response_data == {"Error": "Could not connect to database"} + + +# Case 4: Unauthorized / invalid api key +@patch("api.controllers.question_controller.db") +def test_get_questions_unauthorized(mock_db, test_client): + # Set up the sample bid ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + + # Mock the database find method to return an empty list + mock_db["questions"].find.return_value = [] + + # Make a request to the endpoint to get the questions without providing a JWT + response = test_client.get( + f"api/bids/{sample_bid_id}/questions", headers={"host": "localhost:8080"} + ) + + # Assert the response status code and content + assert response.status_code == 401 + response_data = response.get_json() + assert response_data == {"Error": "Unauthorized"} + + # Make a request to the endpoint to get the questions with an invalid JWT + response = test_client.get( + f"api/bids/{sample_bid_id}/questions", + headers={"host": "localhost:8080", "Authorization": "Bearer INVALID_JWT"}, + ) + + # Assert the response status code and content + assert response.status_code == 401 + response_data = response.get_json() + assert response_data == {"Error": "Unauthorized"} + + +# Case 5: No questions found +@patch("api.controllers.question_controller.db") +def test_no_questions_found(mock_db, test_client, basic_jwt): + # Set up the sample bid ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + + # Mock the database find method to return an empty list + mock_db["questions"].find.return_value = [] + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + # Assert the response status code and content + assert response.status_code == 404 + response_data = response.get_json() + assert response_data == {"Error": "Resource not found"} From 22179544dc49868107d288e604f54d16041fc247 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 2 Aug 2023 17:22:07 +0100 Subject: [PATCH 142/208] refactor: added related bid status to be in_progress criteria; updated tests; updated test data to include range of status enums --- api/controllers/question_controller.py | 5 +- request_examples/delete_question.http | 2 + scripts/test_data/bids.json | 4 +- scripts/test_data/questions.json | 12 +-- tests/test_delete_bid.py | 1 - tests/test_delete_question.py | 103 +++++++++++++++++++++++++ tests/test_post_question.py | 26 ++++++- 7 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 request_examples/delete_question.http create mode 100644 tests/test_delete_question.py diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index d3d9dec..c36e031 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -97,8 +97,11 @@ def delete_question(bid_id, question_id): try: bid_id = validate_id_path(bid_id) question_id = validate_id_path(question_id) + bid = db["bids"].find_one({"_id": bid_id, "status": Status.IN_PROGRESS.value}) + if bid is None: + return showNotFoundError(), 404 data = db["questions"].delete_one({"_id": question_id}) - return data, 204 + return data.raw_result, 204 except ValidationError as e: return showValidationError(e), 400 except Exception: diff --git a/request_examples/delete_question.http b/request_examples/delete_question.http new file mode 100644 index 0000000..49d1317 --- /dev/null +++ b/request_examples/delete_question.http @@ -0,0 +1,2 @@ +DELETE http://localhost:8080/api/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e/questions/b259a93e-7b08-4a80-8daa-8cbb5beebcd9 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlBpcmEiLCJhZG1pbiI6dHJ1ZSwiZXhwIjoxNjkxNTg4NzAyfQ.xTrdupUL7LXt3TRO1EmtMA9M7k-Dt4OfW3RW3qwV3sw \ No newline at end of file diff --git a/scripts/test_data/bids.json b/scripts/test_data/bids.json index 8c35cf0..a150a80 100644 --- a/scripts/test_data/bids.json +++ b/scripts/test_data/bids.json @@ -54,7 +54,7 @@ "questions": "/bids/b4846631-9135-4208-8e37-70eba8f77e15/questions", "self": "/bids/b4846631-9135-4208-8e37-70eba8f77e15" }, - "status": "in_progress", + "status": "completed", "success": [ { "has_score": true, @@ -87,7 +87,7 @@ "questions": "/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e/questions", "self": "/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e" }, - "status": "in_progress", + "status": "deleted", "success": [ { "has_score": true, diff --git a/scripts/test_data/questions.json b/scripts/test_data/questions.json index 262428d..3ec461f 100644 --- a/scripts/test_data/questions.json +++ b/scripts/test_data/questions.json @@ -73,7 +73,7 @@ "respondents": ["JohnDoe"], "response": "This is a negative response.", "score": 1, - "status": "in_progress" + "status": "completed" }, { "_id": "f1b4e0a9-9e4c-4726-9a53-1a64ef684828", @@ -92,7 +92,7 @@ "respondents": ["JaneSmith", "AlexBrown"], "response": "This is the fifth response.", "score": 3, - "status": "in_progress" + "status": "completed" }, { "_id": "67c6b84d-3e8f-4c0e-b4c3-2e46e42e2d2d", @@ -111,7 +111,7 @@ "respondents": ["JohnDoe"], "response": "This is the sixth response.", "score": 2, - "status": "in_progress" + "status": "completed" }, { "_id": "15f13a6e-81d1-49c1-94d2-82bc79a1e968", @@ -130,7 +130,7 @@ "respondents": ["JaneSmith", "AlexBrown"], "response": "This is the seventh response.", "score": 2, - "status": "in_progress" + "status": "deleted" }, { "_id": "3a45123c-24a1-4d7f-b792-45d9dd29fc29", @@ -149,7 +149,7 @@ "respondents": ["JohnDoe", "JaneSmith", "AlexBrown"], "response": "This is the eighth response.", "score": 3, - "status": "in_progress" + "status": "deleted" }, { "_id": "b259a93e-7b08-4a80-8daa-8cbb5beebcd9", @@ -168,7 +168,7 @@ "respondents": ["JohnDoe"], "response": "This is a negative response.", "score": 1, - "status": "in_progress" + "status": "deleted" } ] diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index 8442537..af29a7b 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -13,7 +13,6 @@ def test_delete_bid_success(mock_db, test_client, admin_jwt): headers={"Authorization": f"Bearer {admin_jwt}"}, ) assert response.status_code == 204 - assert response.content_length is None # Case 2: Failed to call database diff --git a/tests/test_delete_question.py b/tests/test_delete_question.py new file mode 100644 index 0000000..9485785 --- /dev/null +++ b/tests/test_delete_question.py @@ -0,0 +1,103 @@ +from unittest.mock import patch + + +# Case 1: Successful hard delete question +@patch("api.controllers.question_controller.db") +def test_delete_question_success(mock_db, test_client, admin_jwt): + 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/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions", + "self": "/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + "was_successful": False, + } + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", + headers={"Authorization": f"Bearer {admin_jwt}"}, + ) + mock_db["bids"].find_one.assert_called_once_with( + {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "in_progress"} + ) + mock_db["questions"].delete_one.assert_called_once_with( + {"_id": "6e7d3f8a-fab3-4ebf-8348-96d0808d325e"} + ) + assert response.status_code == 204 + + +# Case 2: Failed to call database +@patch("api.controllers.question_controller.db") +def test_delete_question_connection_error(mock_db, test_client, admin_jwt): + mock_db["questions"].delete_one.side_effect = Exception + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", + headers={"Authorization": f"Bearer {admin_jwt}"}, + ) + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} + + +# Case 3: Validation error +@patch("api.controllers.question_controller.db") +def test_delete_question_validation_error(mock_db, test_client, admin_jwt): + response = test_client.delete( + "/api/bids/invalid-bid-id/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", + headers={"Authorization": f"Bearer {admin_jwt}"}, + ) + assert response.status_code == 400 + assert response.get_json() == {"Error": "{'id': ['Invalid Id']}"} + + +# Case 4: Related bid not found +@patch("api.controllers.question_controller.db") +def test_delete_question_bid_not_found(mock_db, test_client, admin_jwt): + mock_db["bids"].find_one.return_value = None + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", + headers={"Authorization": f"Bearer {admin_jwt}"}, + ) + + mock_db["bids"].find_one.assert_called_once_with( + {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "in_progress"} + ) + assert response.status_code == 404 + mock_db["questions"].delete_one.assert_not_called() + assert response.get_json() == {"Error": "Resource not found"} + + +# Case 5: Unauthorized - invalid token +@patch("api.controllers.question_controller.db") +def test_delete_question_unauthorized(mock_db, test_client): + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", + headers={"Authorization": "Bearer N0tV4l1djsonW3Bt0K3n"}, + ) + assert response.status_code == 401 + assert response.get_json() == {"Error": "Unauthorized"} + + +# Case 6: Forbidden - not admin +@patch("api.controllers.question_controller.db") +def test_delete_question_forbidden(mock_db, test_client, basic_jwt): + response = test_client.delete( + "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", + headers={"Authorization": f"Bearer {basic_jwt}"}, + ) + assert response.status_code == 403 + assert response.get_json() == {"Error": "Forbidden"} + + +# # Case 7: Idempotence - question not found / already deleted +# @patch("api.controllers.question_controller.db") +# def test_delete_question_idempotence(mock_db, test_client, admin_jwt): +# response = test_client.delete( +# "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", +# headers={"Authorization": f"Bearer {admin_jwt}"}, +# ) +# assert response.status_code == 204 diff --git a/tests/test_post_question.py b/tests/test_post_question.py index f5ed2d9..05f349f 100644 --- a/tests/test_post_question.py +++ b/tests/test_post_question.py @@ -44,7 +44,7 @@ def test_post_is_successful(mock_db, test_client, basic_jwt): # Case 2: Missing mandatory fields @patch("api.controllers.question_controller.db") -def test_field_missing(mock_db, test_client, basic_jwt): +def testpost_question_field_missing(mock_db, test_client, basic_jwt): data = { "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { @@ -108,3 +108,27 @@ def test_post_question_unauthorized(mock_db, test_client): ) assert response.status_code == 401 assert response.get_json() == {"Error": "Unauthorized"} + + +# Case 5: Related bid not found +@patch("api.controllers.question_controller.db") +def test_post_question_bid_not_found(mock_db, test_client, basic_jwt): + data = { + "description": "This is a question", + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + # Mock the behavior of db + mock_db["bids"].find_one.return_value = None + response = test_client.post( + "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + json=data, + headers={"Authorization": f"Bearer {basic_jwt}"}, + ) + + mock_db["bids"].find_one.assert_called_once() + assert response.status_code == 404 + assert response.get_json() == {"Error": "Resource not found"} From 2328479382fd787e01525d379371918ba6254c1c Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 2 Aug 2023 17:38:39 +0100 Subject: [PATCH 143/208] style: updated swagger --- static/swagger_config.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 7d5c7d1..87b8268 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -443,11 +443,9 @@ paths: $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' - # -------------------------------------------- # Components components: -# -------------------------------------------- # Schemas schemas: Bid: @@ -845,7 +843,6 @@ components: score: 10 out_of: 20 respondents: 'ONS' - # -------------------------------------------- # Error responses responses: From 569c561cd72de89ae9ae431556c245793c285810 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 2 Aug 2023 17:46:08 +0100 Subject: [PATCH 144/208] fix: added /api prefix to links model and bids.json test data --- api/models/bid_model.py | 4 ++-- api/models/question_model.py | 4 ++-- scripts/test_data/bids.json | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/models/bid_model.py b/api/models/bid_model.py index 5a12686..f0b4704 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -69,5 +69,5 @@ class LinksModel: """ def __init__(self, bid_id): - self.self = f"/bids/{bid_id}" - self.questions = f"/bids/{bid_id}/questions" + self.self = f"/api/bids/{bid_id}" + self.questions = f"/api/bids/{bid_id}/questions" diff --git a/api/models/question_model.py b/api/models/question_model.py index 2277cbe..f845269 100644 --- a/api/models/question_model.py +++ b/api/models/question_model.py @@ -50,5 +50,5 @@ class LinksModel: """ def __init__(self, question_id, bid_id): - self.self = f"/bids/{bid_id}/questions/{question_id}" - self.bid = f"/bids/{bid_id}" + self.self = f"/api/bids/{bid_id}/questions/{question_id}" + self.bid = f"/api/bids/{bid_id}" diff --git a/scripts/test_data/bids.json b/scripts/test_data/bids.json index a150a80..9208699 100644 --- a/scripts/test_data/bids.json +++ b/scripts/test_data/bids.json @@ -18,8 +18,8 @@ }, "last_updated": "2023-07-20T17:00:40.510224", "links": { - "questions": "/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", - "self": "/bids/be15c306-c85b-4e67-a9f6-682553c065a1" + "questions": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + "self": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1" }, "status": "in_progress", "success": [ @@ -51,8 +51,8 @@ }, "last_updated": "2023-07-20T17:03:12.972780", "links": { - "questions": "/bids/b4846631-9135-4208-8e37-70eba8f77e15/questions", - "self": "/bids/b4846631-9135-4208-8e37-70eba8f77e15" + "questions": "/api/bids/b4846631-9135-4208-8e37-70eba8f77e15/questions", + "self": "/api/bids/b4846631-9135-4208-8e37-70eba8f77e15" }, "status": "completed", "success": [ @@ -84,8 +84,8 @@ }, "last_updated": "2023-07-20T17:03:19.452381", "links": { - "questions": "/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e/questions", - "self": "/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e" + "questions": "/api/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e/questions", + "self": "/api/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e" }, "status": "deleted", "success": [ From ce5dcd566e05e53d449e887fdebcae049d997871 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Thu, 3 Aug 2023 09:46:29 +0100 Subject: [PATCH 145/208] feat: put route for a question by id --- api/controllers/question_controller.py | 33 ++++++++++++++++++++++++-- api/models/question_model.py | 14 +++++++---- api/schemas/question_schema.py | 10 ++++---- helpers/helpers.py | 9 +++++++ request_examples/get_all_question.http | 2 +- request_examples/post_question.http | 6 ++--- tests/test_get_question_by_id.py | 14 +++++++++++ tests/test_get_questions.py | 14 +++++++++++ 8 files changed, 86 insertions(+), 16 deletions(-) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index da127f0..c5bad7e 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -12,7 +12,7 @@ validate_and_create_question_document, validate_bid_id_path, validate_bid_update, - validate_status_update, + validate_status_update_question, prepend_host_to_links, require_api_key, require_jwt, @@ -80,7 +80,7 @@ def get_question(bid_id, question_id): data = db["questions"].find_one( { "_id": question_id, - "links.bid": f"/bids/{bid_id}", + "links.self": f"/bids/{bid_id}/questions/{question_id}", "status": {"$ne": Status.DELETED.value}, } ) @@ -95,3 +95,32 @@ def get_question(bid_id, question_id): return showValidationError(e), 400 except Exception as e: return showInternalServerError(), 500 + + +@question.route("/bids//questions/", methods=["PUT"]) +@require_jwt +def update_question(bid_id, question_id): + try: + bid_id = validate_bid_id_path(bid_id) + question_id = validate_bid_id_path(question_id) + data = db["questions"].find_one( + { + "_id": question_id, + "links.self": f"/bids/{bid_id}/questions/{question_id}", + "status": {"$ne": Status.DELETED.value}, + } + ) + # Return 404 response if not found / returns None + if len(data) == 0: + return showNotFoundError(), 404 + + updated_question = validate_status_update_question(request.get_json(), data) + db["questions"].replace_one({"_id": question_id}, updated_question) + + return updated_question, 200 + except ValidationError as e: + return showValidationError(e), 400 + except UnprocessableEntity as e: + return showUnprocessableEntityError(e), 422 + except Exception as e: + return str(e), 500 diff --git a/api/models/question_model.py b/api/models/question_model.py index 2277cbe..3d3e434 100644 --- a/api/models/question_model.py +++ b/api/models/question_model.py @@ -10,20 +10,20 @@ def __init__( description, question_url, feedback, - bid_id, + bid_id=None, response=None, score=None, out_of=None, respondents=[], status=None, - question_id=None, links=None, last_updated=None, + _id=None, ): - if question_id is None: + if _id is None: self._id = uuid4() else: - self._id = question_id + self._id = _id if status is None: self.status = Status.IN_PROGRESS else: @@ -35,7 +35,11 @@ def __init__( self.score = score self.out_of = out_of self.respondents = respondents - self.links = LinksModel(self._id, bid_id) + if bid_id is None: + self.links = links + else: + self.links = LinksModel(self._id, bid_id) + self.last_updated = datetime.now() diff --git a/api/schemas/question_schema.py b/api/schemas/question_schema.py index f09864e..e0cf85f 100644 --- a/api/schemas/question_schema.py +++ b/api/schemas/question_schema.py @@ -21,11 +21,11 @@ class QuestionSchema(Schema): required=True, error_messages={"required": {"message": "Missing mandatory field"}}, ) - bid_id = fields.UUID(required=True) - response = fields.Str() - score = fields.Integer() - out_of = fields.Integer() - respondents = fields.List(fields.Str()) + bid_id = fields.UUID() + response = fields.Str(allow_none=True) + score = fields.Integer(allow_none=True) + out_of = fields.Integer(allow_none=True) + respondents = fields.List(fields.Str(allow_none=True)) status = fields.Enum(Status, by_value=True) links = fields.Nested(QuestionLinksSchema) last_updated = fields.DateTime() diff --git a/helpers/helpers.py b/helpers/helpers.py index 865b659..31f2cf1 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -150,3 +150,12 @@ def validate_and_create_question_document(request, bid_id): # Serialize to a JSON object data = QuestionSchema().dump(question_document) return data + + +def validate_status_update_question(request, resource): + if request == {}: + raise UnprocessableEntity("Request must not be empty") + resource.update(request) + question_document = QuestionSchema().load(resource, partial=True) + data = QuestionSchema().dump(question_document) + return data diff --git a/request_examples/get_all_question.http b/request_examples/get_all_question.http index d2d7d32..f011fcb 100644 --- a/request_examples/get_all_question.http +++ b/request_examples/get_all_question.http @@ -1,2 +1,2 @@ -GET http://localhost:8080/api/bids/66fb5dba-f129-413a-b12e-5a68b5a647d6/questions HTTP/1.1 +GET http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions/8a96ce4a-0b97-4373-81b3-20ce0e67738a HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU3MDIyNn0.93o-R3VGeNHAbfthiJ4odsA8n9eBu7ZKwU2KP06EE6E \ No newline at end of file diff --git a/request_examples/post_question.http b/request_examples/post_question.http index d0cb27c..a34cc55 100644 --- a/request_examples/post_question.http +++ b/request_examples/post_question.http @@ -1,10 +1,10 @@ -POST http://localhost:8080/api/bids/66fb5dba-f129-441a-b12e-5a68b5a647d6/questions HTTP/1.1 +PUT http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions/3eabf8e2-ed72-4e72-8798-2eb8ead0c1c6 HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e6J1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU3MDIyNn0.93o-R3VGeNHAbfthiJ4odsA8n9eBu7ZKwU2KP06EE6E +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU5MTg3M30.4Kq1IS-kOltqdIAUdQPX_m884kqSmzUnb6Ha6gEgz20 { "description": "This is a question", "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "feedback": {"description": "Good feedback", + "feedback": {"description": "Good YeI!", "url": "https://organisation.sharepoint.com/Docs/dummyfolder"} } diff --git a/tests/test_get_question_by_id.py b/tests/test_get_question_by_id.py index c65fa72..b61fb40 100644 --- a/tests/test_get_question_by_id.py +++ b/tests/test_get_question_by_id.py @@ -168,3 +168,17 @@ def test_no_question_found_by_id(mock_db, test_client, basic_jwt): assert response.status_code == 404 response_data = response.get_json() assert response_data == {"Error": "Resource not found"} + + +# Case 6: Validation error +@patch("api.controllers.question_controller.db") +def test_get_question_by_id_validation_error(mock_db, test_client, basic_jwt): + # Set up the sample question ID + sample_bid_id = "Invalid bid Id" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + response = test_client.get( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + assert response.status_code == 400 + assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index c449ed3..50c8af2 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -190,3 +190,17 @@ def test_no_questions_found(mock_db, test_client, basic_jwt): assert response.status_code == 404 response_data = response.get_json() assert response_data == {"Error": "Resource not found"} + + +# Case 6: Validation error +@patch("api.controllers.question_controller.db") +def test_get_question_by_id_validation_error(mock_db, test_client, basic_jwt): + # Set up the sample question ID + sample_bid_id = "Invalid bid Id" + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + assert response.status_code == 400 + assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} From d5f17bdc1920876e3bd4c8bc420a49104cc16e5a Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Thu, 3 Aug 2023 10:37:39 +0100 Subject: [PATCH 146/208] test: update question --- tests/test_update_question.py | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/test_update_question.py diff --git a/tests/test_update_question.py b/tests/test_update_question.py new file mode 100644 index 0000000..62fa190 --- /dev/null +++ b/tests/test_update_question.py @@ -0,0 +1,47 @@ +from unittest.mock import patch + + +# Case 1: Successful question update +@patch("api.controllers.question_controller.db") +def test_update_question_success(mock_db, test_client, basic_jwt): + # Set up the sample bid ID and question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + sample_updated_question = { + "_id": sample_question_id, + "description": "Updated question description", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/bids/{sample_bid_id}", + "self": f"/bids/{sample_bid_id}/questions/{sample_question_id}", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + } + + # Mock the database find_one method to return the question data + mock_db["questions"].find_one.return_value = sample_updated_question + + # Mock the database replace_one method to return the updated question + mock_db["questions"].replace_one.return_value = sample_updated_question + + # Make a request to the endpoint to update the question + response = test_client.put( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + json=sample_updated_question, + content_type="application/json", + ) + + # Assert the response status code and content + assert response.status_code == 200 + response_data = response.get_json() + assert response_data == sample_updated_question From 805c080f7c44da00c45707c06dd5fce23b956967 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 3 Aug 2023 11:36:20 +0100 Subject: [PATCH 147/208] fix: updated tests for recent refactor --- api/controllers/question_controller.py | 23 +++++++---------------- helpers/helpers.py | 2 +- tests/test_bid_schema.py | 4 ++-- tests/test_delete_question.py | 4 ++-- tests/test_get_question_by_id.py | 2 +- tests/test_get_questions.py | 2 +- tests/test_question_schema.py | 4 ++-- 7 files changed, 16 insertions(+), 25 deletions(-) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index dd9d325..7b11ade 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -11,10 +11,8 @@ showValidationError, validate_and_create_question_document, validate_id_path, - validate_bid_update, - validate_status_update_question, + validate_question_update, prepend_host_to_links, - require_api_key, require_jwt, require_admin_access, ) @@ -60,7 +58,6 @@ def get_questions(bid_id): ) if len(data) == 0: return showNotFoundError(), 404 - for question in data: prepend_host_to_links(question, hostname) return {"total_count": len(data), "items": data}, 200 @@ -86,9 +83,7 @@ def get_question(bid_id, question_id): ) if len(data) == 0: return showNotFoundError(), 404 - prepend_host_to_links(data, hostname) - return data, 200 except ValidationError as e: return showValidationError(e), 400 @@ -96,14 +91,13 @@ def get_question(bid_id, question_id): return showInternalServerError(), 500 - @question.route("/bids//questions/", methods=["DELETE"]) @require_admin_access def delete_question(bid_id, question_id): try: bid_id = validate_id_path(bid_id) question_id = validate_id_path(question_id) - bid = db["bids"].find_one({"_id": bid_id, "status": Status.IN_PROGRESS.value}) + bid = db["bids"].find_one({"_id": bid_id}) if bid is None: return showNotFoundError(), 404 data = db["questions"].delete_one({"_id": question_id}) @@ -113,13 +107,13 @@ def delete_question(bid_id, question_id): except Exception: return showInternalServerError(), 500 - + @question.route("/bids//questions/", methods=["PUT"]) @require_jwt def update_question(bid_id, question_id): try: - bid_id = validate_bid_id_path(bid_id) - question_id = validate_bid_id_path(question_id) + bid_id = validate_id_path(bid_id) + question_id = validate_id_path(question_id) data = db["questions"].find_one( { "_id": question_id, @@ -130,15 +124,12 @@ def update_question(bid_id, question_id): # Return 404 response if not found / returns None if len(data) == 0: return showNotFoundError(), 404 - - updated_question = validate_status_update_question(request.get_json(), data) + updated_question = validate_question_update(request.get_json(), data) db["questions"].replace_one({"_id": question_id}, updated_question) - return updated_question, 200 except ValidationError as e: return showValidationError(e), 400 except UnprocessableEntity as e: return showUnprocessableEntityError(e), 422 except Exception as e: - return str(e), 500 - + return showInternalServerError(), 500 diff --git a/helpers/helpers.py b/helpers/helpers.py index 36f0bf7..308da94 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -152,7 +152,7 @@ def validate_and_create_question_document(request, bid_id): return data -def validate_status_update_question(request, resource): +def validate_question_update(request, resource): if request == {}: raise UnprocessableEntity("Request must not be empty") resource.update(request) diff --git a/tests/test_bid_schema.py b/tests/test_bid_schema.py index a332c67..fd29091 100644 --- a/tests/test_bid_schema.py +++ b/tests/test_bid_schema.py @@ -32,9 +32,9 @@ def test_bid_model(): # 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/{bid_id}" + assert to_post["links"]["self"] == f"/api/bids/{bid_id}" assert "questions" in to_post["links"] - assert to_post["links"]["questions"] == f"/bids/{bid_id}/questions" + assert to_post["links"]["questions"] == f"/api/bids/{bid_id}/questions" # Test that status is set to in_progress assert to_post["status"] == "in_progress" diff --git a/tests/test_delete_question.py b/tests/test_delete_question.py index 9485785..14b1dd0 100644 --- a/tests/test_delete_question.py +++ b/tests/test_delete_question.py @@ -23,7 +23,7 @@ def test_delete_question_success(mock_db, test_client, admin_jwt): headers={"Authorization": f"Bearer {admin_jwt}"}, ) mock_db["bids"].find_one.assert_called_once_with( - {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "in_progress"} + {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9"} ) mock_db["questions"].delete_one.assert_called_once_with( {"_id": "6e7d3f8a-fab3-4ebf-8348-96d0808d325e"} @@ -64,7 +64,7 @@ def test_delete_question_bid_not_found(mock_db, test_client, admin_jwt): ) mock_db["bids"].find_one.assert_called_once_with( - {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "status": "in_progress"} + {"_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9"} ) assert response.status_code == 404 mock_db["questions"].delete_one.assert_not_called() diff --git a/tests/test_get_question_by_id.py b/tests/test_get_question_by_id.py index b61fb40..8947845 100644 --- a/tests/test_get_question_by_id.py +++ b/tests/test_get_question_by_id.py @@ -181,4 +181,4 @@ def test_get_question_by_id_validation_error(mock_db, test_client, basic_jwt): headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, ) assert response.status_code == 400 - assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} + assert response.get_json() == {"Error": "{'id': ['Invalid Id']}"} diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index 50c8af2..c246628 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -203,4 +203,4 @@ def test_get_question_by_id_validation_error(mock_db, test_client, basic_jwt): headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, ) assert response.status_code == 400 - assert response.get_json() == {"Error": "{'bid_id': ['Invalid bid Id']}"} + assert response.get_json() == {"Error": "{'id': ['Invalid Id']}"} diff --git a/tests/test_question_schema.py b/tests/test_question_schema.py index c82a429..a014f73 100644 --- a/tests/test_question_schema.py +++ b/tests/test_question_schema.py @@ -29,9 +29,9 @@ def test_question_model(): # 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/{bid_id}/questions/{question_id}" + assert to_post["links"]["self"] == f"/api/bids/{bid_id}/questions/{question_id}" assert "bid" in to_post["links"] - assert to_post["links"]["bid"] == f"/bids/{bid_id}" + assert to_post["links"]["bid"] == f"/api/bids/{bid_id}" # Test that status is set to in_progress assert to_post["status"] == "in_progress" From f1daed34e57021dea5e3bca0c10cc427bd1b8b74 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 3 Aug 2023 13:15:09 +0100 Subject: [PATCH 148/208] refactor: separated make targets into scripts and tools; updated targets for default makefile so all targets can be run in root folder; updated readmes --- Makefile | 118 ++++++++++--------------- README.md | 56 +++++++----- api/controllers/question_controller.py | 2 +- scripts/Makefile | 27 ++++++ scripts/README.md | 17 +--- scripts/get_jwt.py | 1 + tests/test_post_question.py | 2 +- tools.mk | 44 +++++++++ 8 files changed, 156 insertions(+), 111 deletions(-) create mode 100644 scripts/Makefile create mode 100644 tools.mk diff --git a/Makefile b/Makefile index 83b7bb0..477eccd 100644 --- a/Makefile +++ b/Makefile @@ -1,89 +1,40 @@ .ONESHELL: .DEFAULT_GOAL := run -TOPICS := fix - feat - docs - style - refactor - test - chore - build - PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: run test clean check help commit swagger format branch lint setup bids questions dbclean authserver authplay +.PHONY: help auth clean mongostart mongostop run setup swag test help: - @echo "gmake help - display this help" - @echo "gmake authplay - get JWT and interact with auth api" - @echo "gmake authserver - run auth api application" - @echo "gmake bids - populate bids collection" - @echo "gmake branch - create a new branch" - @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" - @echo "gmake dbclean - clean up the application database" - @echo "gmake format - format the code" - @echo "gmake lint - run linters" - @echo "gmake mongostart - run local mongodb instance" - @echo "gmake mongostop - stop local mongodb instance" - @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" - @echo "gmake questions - populate questions collection" - -authserver: + @echo "make help - display this help" + @echo "make auth - run auth api application" + @echo "make build - create and activate virtual environment" + @echo "make clean - remove all generated files" + @echo "make mongostart - start local mongodb instance" + @echo "make mongostop - stop local mongodb instance" + @echo "make run - run the application" + @echo "make swag - open swagger documentation" + @echo "make setup - setup the application database" + @echo "make test - run tests and coverage report" + @echo "make helptools - display help for tools" + +auth: $(PYTHON) ../tdse-accessForce-auth-stub/app.py -authplay: - @echo "Getting JWT..." - @find . -name "get_jwt.py" -exec python3 {} \; - -bids: - @echo "Creating bids..." - @find . -name "create_bids.py" -exec python3 {} \; - -branch: - @echo "Available branch types:" - @echo "$(TOPICS)" - @read -p "Enter the branch type: " type; \ - read -p "Enter the branch description (kebab-case): " description; \ - git checkout -b $${type}/$${description}; \ - git push --set-upstream origin $${type}/$${description} - -build: venv/bin/activate +build: requirements.txt + python3 -m venv .venv + $(PIP) install -r requirements.txt . ./.venv/bin/activate -check: - $(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)" - @read -p "Enter the topic for the commit: " topic; \ - read -p "Enter the commit message: " message; \ - git add .; \ - git commit -m "$${topic}: $${message}"; \ - git push - -dbclean: - @echo "Cleaning up database..." - @find . -name "delete_bids.py" -exec python3 {} \; - @find . -name "delete_questions.py" -exec python3 {} \; - -format: - $(PYTHON) -m black . - -lint: - $(PYTHON) -m flake8 - $(PYTHON) -m pylint **/*.py **/**/*.py *.py - mongostart: @echo "Starting MongoDB..." brew services start mongodb-community@6.0 @@ -95,7 +46,12 @@ mongostop: run: build $(PYTHON) app.py -setup: dbclean bids questions +setup: + @echo "Setting up application database..." + cd ./scripts/; \ + make dbclean; \ + make bids; \ + make questions @echo "Database setup complete." swag: @@ -106,11 +62,27 @@ test: @echo "TEST COVERAGE REPORT" coverage report -m --omit="tests/*,dbconfig/*" -questions: - @echo "Creating questions..." - @find . -name "create_questions.py" -exec python3 {} \; -venv/bin/activate: requirements.txt - python3 -m venv .venv - $(PIP) install -r requirements.txt +.PHONY: helptools authplay branch check commit format lint + +helptools: + make -f tools.mk help + +authplay: + cd ./scripts/; \ + make authplay + +branch: + make -f tools.mk branch + +check: + make -f tools.mk check + +commit: + make -f tools.mk commit +format: + make -f tools.mk format + +lint: + make -f tools.mk lint diff --git a/README.md b/README.md index 8d807d6..df00063 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,12 @@ This API stores and serves information about Methods bids for client tenders. 6. Run the following command to have all the commands to use the API with Makefile: ```bash - gmake help + make help ``` 7. Run the following command to start the API: ```bash - gmake run + make run ``` * The API will be available at http://localhost:8080/api/bids @@ -54,23 +54,39 @@ This API stores and serves information about Methods bids for client tenders. 9. In a new terminal enter the following command to run authorization server if not already running. This will be needed to generate a token: ```bash - gmake authserver + make auth ``` * The API will be available at http://localhost:5000/authorise -------------- +## Database Setup + +To set up the application database, run the following command: + + ```bash + make setup + ``` + +This will perform the following steps: + +1. Clean up the existing database +2. Populate the bids collection with test data +3. Populate the questions collection with test data, using existing bid IDs + +-------------- + ## Accessing API Documentation (Swagger Specification) 1. Run the following command to start the API: ```bash - gmake run + make run ``` 2. In a new terminal run the following command to open the Swagger UI in your default web browser: ```bash - gmake swag + make swag ``` -------------- @@ -79,28 +95,15 @@ This API stores and serves information about Methods bids for client tenders. 1. Run the following command to start the API: ```bash - gmake run + make run ``` 2. In a new terminal enter the following command to run the test suites and generate a test coverage report: ```bash - gmake test + make test ``` -------------- -## Using auth playground to generate a token - -1. Follow the steps in the section above to start the API and authorization server. - -2. In a new terminal enter the following command to open the auth playground in your default web browser: - - ```bash - gmake authplay - ``` -3. Follow the steps in the auth playground to generate a token and much more. - --------------- - ## 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/) @@ -151,6 +154,19 @@ OPTIONAL - Download MongoDB Compass to view the database in a GUI. You can downl -------------- +## Using auth playground to generate a token and make authenticated requests to the Bids API + +1. Follow the steps in the section above to start the API and authorization server. + +2. In a new terminal enter the following command to open the auth playground in your default web browser: + + ```bash + make authplay + ``` +3. Follow the steps in the auth playground to generate a token and much more. + +-------------- + ### 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/question_controller.py b/api/controllers/question_controller.py index 7b11ade..a193855 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -131,5 +131,5 @@ def update_question(bid_id, question_id): return showValidationError(e), 400 except UnprocessableEntity as e: return showUnprocessableEntityError(e), 422 - except Exception as e: + except Exception: return showInternalServerError(), 500 diff --git a/scripts/Makefile b/scripts/Makefile new file mode 100644 index 0000000..a6b6d9b --- /dev/null +++ b/scripts/Makefile @@ -0,0 +1,27 @@ +.ONESHELL: + +.PHONY: help authplay bids dbclean questions + +help: + @echo "make help - display this help" + @echo "make authplay - get JWT and interact with auth api" + @echo "make bids - populate bids collection" + @echo "make dbclean - clean up the application database" + @echo "make questions - populate questions collection" + +authplay: + @echo "Getting JWT..." + @find . -name "get_jwt.py" -exec python3 {} \; + +bids: + @echo "Creating bids..." + @find . -name "create_bids.py" -exec python3 {} \; + +dbclean: + @echo "Cleaning up database..." + @find . -name "delete_bids.py" -exec python3 {} \; + @find . -name "delete_questions.py" -exec python3 {} \; + +questions: + @echo "Creating questions..." + @find . -name "create_questions.py" -exec python3 {} \; \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md index b904001..327dfe3 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -12,7 +12,7 @@ To run the database cleanup script, execute the following command: ```bash -gmake dbclean +make dbclean ``` Or to run the cleanup script for only the bids collection, execute: @@ -34,18 +34,3 @@ To run the sample questions data population script, execute the following comman ```bash python3 create_questions.py ``` - -## Database Setup - -### Script Description -The setup target in the Makefile sets up the application database by performing the following steps: - -1. Cleaning up the existing database using the dbclean target. -2. Creating sample data using the bids target. -3. Creating sample data using the questions target. - -### Usage -To set up the application database, run the following command: -```bash -gmake setup -``` \ No newline at end of file diff --git a/scripts/get_jwt.py b/scripts/get_jwt.py index 2a5e4e0..10a9ee8 100644 --- a/scripts/get_jwt.py +++ b/scripts/get_jwt.py @@ -134,6 +134,7 @@ def main(): print("Invalid choice. Try again.") else: print("Login failed. Try again.") + return if __name__ == "__main__": diff --git a/tests/test_post_question.py b/tests/test_post_question.py index 05f349f..ffd6aa9 100644 --- a/tests/test_post_question.py +++ b/tests/test_post_question.py @@ -44,7 +44,7 @@ def test_post_is_successful(mock_db, test_client, basic_jwt): # Case 2: Missing mandatory fields @patch("api.controllers.question_controller.db") -def testpost_question_field_missing(mock_db, test_client, basic_jwt): +def test_post_question_field_missing(mock_db, test_client, basic_jwt): data = { "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", "feedback": { diff --git a/tools.mk b/tools.mk new file mode 100644 index 0000000..1e6db23 --- /dev/null +++ b/tools.mk @@ -0,0 +1,44 @@ +.ONESHELL: + +TOPICS := fix - feat - docs - style - refactor - test - chore - build + +PYTHON = ./.venv/bin/python3 +PIP = ./.venv/bin/pip + +.PHONY: help branch check commit format lint + +help: + @echo "make helptools - display this help" + @echo "make branch - create a new branch" + @echo "make check - check for security vulnerabilities" + @echo "make commit - commit changes to git" + @echo "make format - format the code" + @echo "make lint - run linters" + +branch: + @echo "Available branch types:" + @echo "$(TOPICS)" + @read -p "Enter the branch type: " type; \ + read -p "Enter the branch description (kebab-case): " description; \ + git checkout -b $${type}/$${description}; \ + git push --set-upstream origin $${type}/$${description} + +check: + $(PIP) install safety + $(PIP) freeze | $(PYTHON) -m safety check --stdin + +commit: format + @echo "Available topics:" + @echo "$(TOPICS)" + @read -p "Enter the topic for the commit: " topic; \ + read -p "Enter the commit message: " message; \ + git add .; \ + git commit -m "$${topic}: $${message}"; \ + git push + +format: + $(PYTHON) -m black . + +lint: + $(PYTHON) -m flake8 + $(PYTHON) -m pylint **/*.py **/**/*.py *.py \ No newline at end of file From 597924da15460a0c63b46140aed475ee39aae503 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 3 Aug 2023 14:20:13 +0100 Subject: [PATCH 149/208] test: added assertion to test_update_bid_by_id_success --- tests/test_update_bid_by_id.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index 0beceab..d45d16f 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -26,6 +26,7 @@ def test_update_bid_by_id_success(mock_db, test_client, basic_jwt): ) mock_db["bids"].replace_one.assert_called_once() assert response.status_code == 200 + assert response.get_json()["tender"] == "UPDATED TENDER" # Case 2: Invalid user input From 2b74c68b36210fc4a5cac3123407ad70fba5fc8c Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 3 Aug 2023 14:28:02 +0100 Subject: [PATCH 150/208] fix: added flag to make test target to run coverage report after failing test rather than exit with error --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 477eccd..73fc81f 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ swag: open http://localhost:8080/api/docs/#/ test: - coverage run -m pytest -vv + -coverage run -m pytest -vv @echo "TEST COVERAGE REPORT" coverage report -m --omit="tests/*,dbconfig/*" From 1b207cd52d5ecbf3f382891e278332c70e79092a Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 3 Aug 2023 14:29:39 +0100 Subject: [PATCH 151/208] test: added assertion to test_update_bid_status_success --- tests/test_update_bid_status.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index 733e857..75dca14 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -33,6 +33,7 @@ def test_update_bid_status_success(mock_db, test_client, admin_jwt): 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 + assert response.get_json()["status"] == "completed" # Case 2: Invalid status From 76915cdaa6d81a05e91a766ebcd0a287a1208294 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Thu, 3 Aug 2023 14:54:03 +0100 Subject: [PATCH 152/208] test: update question --- api/controllers/question_controller.py | 1 + tests/test_update_question.py | 123 ++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 7b11ade..bb61bbf 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -124,6 +124,7 @@ def update_question(bid_id, question_id): # Return 404 response if not found / returns None if len(data) == 0: return showNotFoundError(), 404 + updated_question = validate_question_update(request.get_json(), data) db["questions"].replace_one({"_id": question_id}, updated_question) return updated_question, 200 diff --git a/tests/test_update_question.py b/tests/test_update_question.py index 62fa190..8b36077 100644 --- a/tests/test_update_question.py +++ b/tests/test_update_question.py @@ -14,7 +14,6 @@ def test_update_question_success(mock_db, test_client, basic_jwt): "description": "Good feedback", "url": "https://organisation.sharepoint.com/Docs/dummyfolder", }, - "last_updated": "2023-08-01T23:11:59.336092", "links": { "bid": f"/bids/{sample_bid_id}", "self": f"/bids/{sample_bid_id}/questions/{sample_question_id}", @@ -43,5 +42,127 @@ def test_update_question_success(mock_db, test_client, basic_jwt): # Assert the response status code and content assert response.status_code == 200 + response_data = response.get_json() + assert response_data["last_updated"] is not None + # Remove the 'last_updated' field from the response data before comparison + response_data.pop("last_updated", None) assert response_data == sample_updated_question + + +# Case 2: Invalid user input +@patch("api.controllers.question_controller.db") +def test_update_question_invalid_input(mock_db, test_client, basic_jwt): + # Set up the sample bid ID and question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + mock_db["questions"].find_one.return_value = { + "_id": sample_question_id, + "description": "Updated question description", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "links": { + "bid": f"/bids/{sample_bid_id}", + "self": f"/bids/{sample_bid_id}/questions/{sample_question_id}", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + } + update = {"description": 42} + # Make a request to the endpoint to update the question with invalid data + response = test_client.put( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + json=update, + content_type="application/json", + ) + + # Assert the response status code and content + assert response.status_code == 400 + response_data = response.get_json()["Error"] + assert response_data == "{'description': ['Not a valid string.']}" + + +# Case 3: Question not found +@patch("api.controllers.question_controller.db") +def test_question_not_found(mock_db, test_client, basic_jwt): + # Set up the sample bid ID and question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + mock_db["questions"].find_one.return_value = {} + + # Make a request to the endpoint to update the question with invalid data + response = test_client.put( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + json={}, + content_type="application/json", + ) + # Assert the response status code and content + assert response.status_code == 404 + response_data = response.get_json()["Error"] + assert response_data == "Resource not found" + + +# Case 4: Exception handling - Internal Server Error +@patch("api.controllers.question_controller.db") +def test_exception_internal_server_error(mock_db, test_client, basic_jwt): + # Set up the sample bid ID and question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + # Mock the database find_one method to raise an Exception + mock_db["questions"].find_one.side_effect = Exception("Test Exception") + update = {"tender": "Updated tender"} + response = test_client.put( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + json=update, + content_type="application/json", + ) + assert response.status_code == 500 + assert response.get_json() == {"Error": "Could not connect to database"} + + +# Case 5: Empty request body +@patch("api.controllers.question_controller.db") +def test_update_question_invalid_empty_request_body(mock_db, test_client, basic_jwt): + # Set up the sample bid ID and question ID + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" + mock_db["questions"].find_one.return_value = { + "_id": sample_question_id, + "description": "Updated question description", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "links": { + "bid": f"/bids/{sample_bid_id}", + "self": f"/bids/{sample_bid_id}/questions/{sample_question_id}", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + } + update = {} + # Make a request to the endpoint to update the question with invalid data + response = test_client.put( + f"api/bids/{sample_bid_id}/questions/{sample_question_id}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + json=update, + content_type="application/json", + ) + + # Assert the response status code and content + assert response.status_code == 422 + response_data = response.get_json()["Error"] + assert response_data == "Request must not be empty" From bde6d308937b40500a948d5e93ba5783a89ba020 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 4 Aug 2023 12:13:36 +0100 Subject: [PATCH 153/208] test: changed dbconfig to use env variables; added conditional test environment db settings; added test-setup target to makefile --- Makefile | 12 +++++++++-- api/controllers/question_controller.py | 1 - dbconfig/mongo_setup.py | 10 +++++++--- scripts/create_bids.py | 11 +++++++--- scripts/create_questions.py | 11 +++++++--- scripts/delete_bids.py | 11 +++++++--- scripts/delete_questions.py | 11 +++++++--- tests/conftest.py | 9 +++------ .../post_and_delete_admin.py | 0 .../post_bid_jwt.py | 0 .../integration/test_get_bids_with_apikey.py | 13 ++++++++++++ tests/integration_tests/get_bids_apikey.py | 20 ------------------- 12 files changed, 65 insertions(+), 44 deletions(-) rename tests/{integration_tests => integration}/post_and_delete_admin.py (100%) rename tests/{integration_tests => integration}/post_bid_jwt.py (100%) create mode 100644 tests/integration/test_get_bids_with_apikey.py delete mode 100644 tests/integration_tests/get_bids_apikey.py diff --git a/Makefile b/Makefile index 73fc81f..919f6a5 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: help auth clean mongostart mongostop run setup swag test +.PHONY: help auth clean mongostart mongostop run setup swag test test-setup help: @echo "make help - display this help" @@ -19,6 +19,7 @@ help: @echo "make swag - open swagger documentation" @echo "make setup - setup the application database" @echo "make test - run tests and coverage report" + @echo "make test-setup - setup the test database" @echo "make helptools - display help for tools" auth: @@ -60,8 +61,15 @@ swag: test: -coverage run -m pytest -vv @echo "TEST COVERAGE REPORT" - coverage report -m --omit="tests/*,dbconfig/*" + coverage report -m --omit="app.py,tests/*,dbconfig/*" +test-setup: + export TEST_ENVIRONMENT=true; \ + cd ./scripts/; \ + make dbclean; \ + make bids; \ + make questions; \ + export TEST_ENVIRONMENT= .PHONY: helptools authplay branch check commit format lint diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 42a25d0..f81dfd2 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -118,7 +118,6 @@ def update_question(bid_id, question_id): { "_id": question_id, "links.self": f"/bids/{bid_id}/questions/{question_id}", - "status": {"$ne": Status.DELETED.value}, } ) # Return 404 response if not found / returns None diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index 9c9f08e..ad7619d 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -8,9 +8,13 @@ load_dotenv() -MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" +DB_HOST = os.getenv("DB_HOST") +DB_PORT = 27017 +DB_NAME = os.getenv("DB_NAME") +if os.environ.get("TEST_ENVIRONMENT"): + DB_NAME = os.getenv("TEST_DB_NAME") # Create a new client and connect to the server -client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) -db = client["bidsAPI"] +client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) +db = client[DB_NAME] diff --git a/scripts/create_bids.py b/scripts/create_bids.py index 1beca63..a836f21 100644 --- a/scripts/create_bids.py +++ b/scripts/create_bids.py @@ -13,7 +13,12 @@ load_dotenv() -MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" +DB_HOST = os.getenv("DB_HOST") +DB_PORT = 27017 +DB_NAME = os.getenv("DB_NAME") + +if os.environ.get("TEST_ENVIRONMENT"): + DB_NAME = os.getenv("TEST_DB_NAME") def populate_bids(): @@ -21,8 +26,8 @@ def populate_bids(): Populates the MongoDB database with sample bids data from bids.json file. """ try: - client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) - data_base = client["bidsAPI"] + client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) + data_base = client[DB_NAME] collection = data_base["bids"] # Get the current script's directory diff --git a/scripts/create_questions.py b/scripts/create_questions.py index 1f5f096..38210d7 100644 --- a/scripts/create_questions.py +++ b/scripts/create_questions.py @@ -14,7 +14,12 @@ load_dotenv() -MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" +DB_HOST = os.getenv("DB_HOST") +DB_PORT = 27017 +DB_NAME = os.getenv("DB_NAME") + +if os.environ.get("TEST_ENVIRONMENT"): + DB_NAME = os.getenv("TEST_DB_NAME") def populate_questions(): @@ -22,8 +27,8 @@ def populate_questions(): Populates the MongoDB database with sample questions data from questions.json file. """ try: - client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) - data_base = client["bidsAPI"] + client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) + data_base = client[DB_NAME] collection = data_base["questions"] # Get the current script's directory diff --git a/scripts/delete_bids.py b/scripts/delete_bids.py index 128fc19..29e9728 100644 --- a/scripts/delete_bids.py +++ b/scripts/delete_bids.py @@ -11,7 +11,12 @@ load_dotenv() -MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" +DB_HOST = os.getenv("DB_HOST") +DB_PORT = 27017 +DB_NAME = os.getenv("DB_NAME") + +if os.environ.get("TEST_ENVIRONMENT"): + DB_NAME = os.getenv("TEST_DB_NAME") def delete_bids(): @@ -19,8 +24,8 @@ def delete_bids(): Deletes all bids from the MongoDB collection. """ try: - client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) - data_base = client["bidsAPI"] + client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) + data_base = client[DB_NAME] collection = data_base["bids"] if collection.count_documents({}) == 0: diff --git a/scripts/delete_questions.py b/scripts/delete_questions.py index bfc662a..b281c15 100644 --- a/scripts/delete_questions.py +++ b/scripts/delete_questions.py @@ -11,7 +11,12 @@ load_dotenv() -MONGO_URI = os.getenv("MONGO_URL") or "mongodb://localhost:27017/bidsAPI" +DB_HOST = os.getenv("DB_HOST") +DB_PORT = 27017 +DB_NAME = os.getenv("DB_NAME") + +if os.environ.get("TEST_ENVIRONMENT"): + DB_NAME = os.getenv("TEST_DB_NAME") def delete_bids(): @@ -19,8 +24,8 @@ def delete_bids(): Deletes all bids from the MongoDB collection. """ try: - client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=10000) - data_base = client["bidsAPI"] + client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) + data_base = client[DB_NAME] collection = data_base["questions"] if collection.count_documents({}) == 0: diff --git a/tests/conftest.py b/tests/conftest.py index ae6611d..1095e99 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,16 @@ import jwt import pytest import os +from app import app from dotenv import load_dotenv -from flask import Flask -from api.controllers.bid_controller import bid -from api.controllers.question_controller import question @pytest.fixture(scope="session") def test_client(): - app = Flask(__name__) - app.register_blueprint(bid, url_prefix="/api") - app.register_blueprint(question, url_prefix="/api") + os.environ["TEST_ENVIRONMENT"] = "True" with app.test_client() as client: yield client + os.environ.pop("TEST_ENVIRONMENT") @pytest.fixture(scope="session") diff --git a/tests/integration_tests/post_and_delete_admin.py b/tests/integration/post_and_delete_admin.py similarity index 100% rename from tests/integration_tests/post_and_delete_admin.py rename to tests/integration/post_and_delete_admin.py diff --git a/tests/integration_tests/post_bid_jwt.py b/tests/integration/post_bid_jwt.py similarity index 100% rename from tests/integration_tests/post_bid_jwt.py rename to tests/integration/post_bid_jwt.py diff --git a/tests/integration/test_get_bids_with_apikey.py b/tests/integration/test_get_bids_with_apikey.py new file mode 100644 index 0000000..110c537 --- /dev/null +++ b/tests/integration/test_get_bids_with_apikey.py @@ -0,0 +1,13 @@ +import requests + + +def test_get_bids_with_api_key(): + auth_response = requests.get("http://localhost:5000/authorise/") + + api_key = auth_response.json()["API_KEY"] + + headers = {"Content-Type": "application/json", "X-API-Key": api_key} + + get_response = requests.get("http://localhost:8080/api/bids", headers=headers) + + assert get_response.status_code == 200 diff --git a/tests/integration_tests/get_bids_apikey.py b/tests/integration_tests/get_bids_apikey.py deleted file mode 100644 index 1160c79..0000000 --- a/tests/integration_tests/get_bids_apikey.py +++ /dev/null @@ -1,20 +0,0 @@ -import requests - -response = requests.get("http://localhost:5000/authorise/") - -api_key = response.json()["API_KEY"] - -print(api_key) - -data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-06-23", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -} - -headers = {"Content-Type": "application/json", "X-API-Key": api_key} - -get_response = requests.get("http://localhost:8080/api/bids", headers=headers) - -print(get_response) From 13e7eb5d3c75d7297f5aab7b72c5d9250dfff536 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 4 Aug 2023 12:21:15 +0100 Subject: [PATCH 154/208] refactor: added terminal feedback for test-setup target --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 919f6a5..2a6d474 100644 --- a/Makefile +++ b/Makefile @@ -64,12 +64,14 @@ test: coverage report -m --omit="app.py,tests/*,dbconfig/*" test-setup: + @echo "Setting up test database..." export TEST_ENVIRONMENT=true; \ cd ./scripts/; \ make dbclean; \ make bids; \ make questions; \ export TEST_ENVIRONMENT= + @echo "Test database setup complete." .PHONY: helptools authplay branch check commit format lint From c4c8648d014d100c713fd044a0c28e026655fd60 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 4 Aug 2023 13:27:36 +0100 Subject: [PATCH 155/208] refactor: added dbclean target to root makefile --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2a6d474..6379bac 100644 --- a/Makefile +++ b/Makefile @@ -6,13 +6,14 @@ PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: help auth clean mongostart mongostop run setup swag test test-setup +.PHONY: help auth clean dbclean mongostart mongostop run setup swag test test-setup help: @echo "make help - display this help" @echo "make auth - run auth api application" @echo "make build - create and activate virtual environment" @echo "make clean - remove all generated files" + @echo "make dbclean - clear the application database" @echo "make mongostart - start local mongodb instance" @echo "make mongostop - stop local mongodb instance" @echo "make run - run the application" @@ -36,6 +37,12 @@ clean: @find . -name ".pytest_cache" -exec rm -rf {} + @find . -name ".venv" -exec rm -rf {} + +dbclean: + @echo "Cleaning up database..." + cd ./scripts/; \ + make dbclean + @echo "Database cleared." + mongostart: @echo "Starting MongoDB..." brew services start mongodb-community@6.0 From 5d930edeffdaf31d1dd3df83e33b321282648677 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 4 Aug 2023 13:56:10 +0100 Subject: [PATCH 156/208] docs: updated readme --- README.md | 111 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index df00063..f27a182 100644 --- a/README.md +++ b/README.md @@ -60,50 +60,20 @@ This API stores and serves information about Methods bids for client tenders. -------------- -## Database Setup - -To set up the application database, run the following command: +## Environmental Variables - ```bash - make setup - ``` +In order to validate your credentials and configure the database connection, you will have to set up the environmental variables locally. -This will perform the following steps: +To do this, create a `.env` file in your root folder, with the following key/value pairs: -1. Clean up the existing database -2. Populate the bids collection with test data -3. Populate the questions collection with test data, using existing bid IDs + API_KEY=THIS_IS_THE_API_KEY + SECRET_KEY=THIS_IS_A_SECRET + DB_HOST=localhost + DB_NAME=bidsAPI + TEST_DB_NAME=testAPI -------------- -## Accessing API Documentation (Swagger Specification) - -1. Run the following command to start the API: - - ```bash - make run - ``` -2. In a new terminal run the following command to open the Swagger UI in your default web browser: - - ```bash - make swag - ``` --------------- - -## Testing the application - -1. Run the following command to start the API: - - ```bash - make run - ``` -2. In a new terminal enter the following command to run the test suites and generate a test coverage report: - - ```bash - make test - ``` --------------- - ## 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/) @@ -145,6 +115,11 @@ This will perform the following steps: ```bash use bidsAPI ``` +8. To create a test database called `testAPI`, run: + + ```bash + use testAPI + ``` 8. To exit the MongoDB shell, run the following command: ```bash @@ -154,6 +129,66 @@ OPTIONAL - Download MongoDB Compass to view the database in a GUI. You can downl -------------- +## Database Setup + +To set up the application database, run the following command: + + ```bash + make setup + ``` + +This will perform the following steps: + +1. Clean up the existing database +2. Populate the bids collection with dummy data +3. Populate the questions collection with dummy data, using existing bid IDs + +-------------- + +## Accessing API Documentation (Swagger Specification) + +1. Run the following command to start the API: + + ```bash + make run + ``` +2. In a new terminal run the following command to open the Swagger UI in your default web browser: + + ```bash + make swag + ``` +-------------- + +## Testing the application + +1. Run the following command to start the API (if not already started): + + ```bash + make run + ``` +2. Run the following command to start the Auth stub (if not already started): + + ```bash + make auth + ``` +3. Run the following command to start the MongoDB instance (if not already started): + + ```bash + make mongostart + ``` +4. Run the following command to setup the test database: + + ```bash + make test-setup + ``` +5. In a new terminal enter the following command to run the test suites and generate a test coverage report: + + ```bash + make test + ``` + +-------------- + ## Using auth playground to generate a token and make authenticated requests to the Bids API 1. Follow the steps in the section above to start the API and authorization server. From bda4a132037f8d59684a249b76d5685eca4d2315 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 4 Aug 2023 14:05:28 +0100 Subject: [PATCH 157/208] docs: removed repeated steps in readme --- README.md | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f27a182..88dcdf0 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ To do this, create a `.env` file in your root folder, with the following key/val ```bash use bidsAPI ``` -8. To create a test database called `testAPI`, run: +8. To create a new test database called `testAPI`, run: ```bash use testAPI @@ -145,14 +145,14 @@ This will perform the following steps: -------------- -## Accessing API Documentation (Swagger Specification) +## Accessing API Documentation (OAS) -1. Run the following command to start the API: +1. Run the following command to start the API (if you haven't already): ```bash make run ``` -2. In a new terminal run the following command to open the Swagger UI in your default web browser: +2. In a new terminal run the following command to view the Swagger UI in your default web browser: ```bash make swag @@ -161,37 +161,23 @@ This will perform the following steps: ## Testing the application -1. Run the following command to start the API (if not already started): +1. Follow the steps above to start the API, authorization server and database connection (if you haven't already). - ```bash - make run - ``` -2. Run the following command to start the Auth stub (if not already started): - - ```bash - make auth - ``` -3. Run the following command to start the MongoDB instance (if not already started): - - ```bash - make mongostart - ``` -4. Run the following command to setup the test database: +2. Run the following command to setup the test database: ```bash make test-setup ``` -5. In a new terminal enter the following command to run the test suites and generate a test coverage report: +3. Enter the following command to run the test suites and generate a test coverage report: ```bash make test ``` - -------------- ## Using auth playground to generate a token and make authenticated requests to the Bids API -1. Follow the steps in the section above to start the API and authorization server. +1. Follow the steps above to start the API, authorization server and database connection (if you haven't already). 2. In a new terminal enter the following command to open the auth playground in your default web browser: From f4e6bd8bd0e1b381e8d8bafcc2a584c4e25c8ff8 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 4 Aug 2023 14:20:29 +0100 Subject: [PATCH 158/208] docs: removed unnecessary steps to install pre-requisites --- README.md | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 88dcdf0..8be0ec8 100644 --- a/README.md +++ b/README.md @@ -14,44 +14,39 @@ This API stores and serves information about Methods bids for client tenders. 1. Clone the repository to your local machine: ```bash - git clone + git clone ``` 2. Navigate to the root directory of the project: ```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: - - ```bash - python3 --version - ``` -4. Install Makefile if not already installed. You can check if it is installed by running the following command: +3. Install Makefile if not already installed. You can check if it is installed by running the following command: ```bash make --version ``` -5. Version 3.81 or higher is required. If you do not have Make installed, you can install it with Homebrew: +4. Version 3.81 or higher is required. If you do not have Make installed, you can install it with Homebrew: ```bash brew install make ``` -6. Run the following command to have all the commands to use the API with Makefile: +5. Run the following command to have all the commands to use the API with Makefile: ```bash make help ``` -7. Run the following command to start the API: +6. Run the following command to start the API: ```bash make run ``` * The API will be available at http://localhost:8080/api/bids -8. Follow this link to go to the authorization documentation: [Authorization Documentation](https://github.com/methods/tdse-accessForce-auth-stub/blob/main/README.md) +7. Follow this link to go to the authorization documentation: [Authorization Documentation](https://github.com/methods/tdse-accessForce-auth-stub/blob/main/README.md) -9. In a new terminal enter the following command to run authorization server if not already running. This will be needed to generate a token: +8. In a new terminal enter the following command to run authorization server if not already running. This will be needed to generate a token: ```bash make auth @@ -78,44 +73,39 @@ To do this, create a `.env` file in your root folder, with the following key/val ### 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: +1. Install MongoDB by running the following commands: ```bash brew tap mongodb/brew brew install mongodb-community ``` -3. To run MongoDB (i.e. the mongod process) as a macOS service, run: +2. To run MongoDB (i.e. the mongod process) as a macOS service, run: ```bash make mongostart ``` -4. To verify that MongoDB is running, run: +3. 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: +4. Run the following command to stop the MongoDB instance, as needed: ```bash make mongostop ``` -6. To begin using MongoDB, connect the MongoDB shell (mongosh) to the running instance. From a new terminal, issue the following: +5. To begin using MongoDB, connect the MongoDB shell (mongosh) to the running instance. From a new terminal, issue the following: ```bash mongosh ``` -7. To create a new database called `bidsAPI`, run: +6. To create a new database called `bidsAPI`, run: ```bash use bidsAPI ``` -8. To create a new test database called `testAPI`, run: +7. To create a new test database called `testAPI`, run: ```bash use testAPI From 23dced3879a530de83b54b4a88b84bf9ea29f936 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Fri, 4 Aug 2023 14:40:16 +0100 Subject: [PATCH 159/208] fix: flake8 compliance --- .flake8 | 1 - api/controllers/question_controller.py | 3 +-- scripts/create_bids.py | 2 +- scripts/create_questions.py | 2 +- scripts/delete_bids.py | 2 +- scripts/delete_questions.py | 2 +- tests/test_get_questions.py | 4 ++-- tests/test_question_schema.py | 2 +- 8 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.flake8 b/.flake8 index 34bb0c5..4cebcf7 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,3 @@ max-line-length = 120 exclude = .venv *.json -ignore= F811,W503, \ No newline at end of file diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 42a25d0..4d16bce 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -1,4 +1,3 @@ -from datetime import datetime from flask import Blueprint, request from marshmallow import ValidationError from werkzeug.exceptions import UnprocessableEntity @@ -87,7 +86,7 @@ def get_question(bid_id, question_id): return data, 200 except ValidationError as e: return showValidationError(e), 400 - except Exception as e: + except Exception: return showInternalServerError(), 500 diff --git a/scripts/create_bids.py b/scripts/create_bids.py index 1beca63..c91c3eb 100644 --- a/scripts/create_bids.py +++ b/scripts/create_bids.py @@ -46,7 +46,7 @@ def populate_bids(): print(f"Inserted bid with _id: {bid['_id']}") except ConnectionFailure: - print(f"Error: Failed to connect to database") + print("Error: Failed to connect to database") sys.exit(1) finally: diff --git a/scripts/create_questions.py b/scripts/create_questions.py index 1f5f096..97938dd 100644 --- a/scripts/create_questions.py +++ b/scripts/create_questions.py @@ -71,7 +71,7 @@ def populate_questions(): print(f"Inserted question with _id: {question['_id']}") except ConnectionFailure: - print(f"Error: Failed to connect to database") + print("Error: Failed to connect to database") sys.exit(1) finally: diff --git a/scripts/delete_bids.py b/scripts/delete_bids.py index 128fc19..3a6776e 100644 --- a/scripts/delete_bids.py +++ b/scripts/delete_bids.py @@ -30,7 +30,7 @@ def delete_bids(): print(f"Deleted {delete_result.deleted_count} bids from the collection.") except ConnectionFailure: - print(f"Error: Failed to connect to database") + print("Error: Failed to connect to database") sys.exit(1) finally: diff --git a/scripts/delete_questions.py b/scripts/delete_questions.py index bfc662a..2c69e78 100644 --- a/scripts/delete_questions.py +++ b/scripts/delete_questions.py @@ -32,7 +32,7 @@ def delete_bids(): ) except ConnectionFailure: - print(f"Error: Failed to connect to database") + print("Error: Failed to connect to database") sys.exit(1) finally: diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index c246628..41069a2 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -35,8 +35,8 @@ def test_get_questions_success(mock_db, test_client, basic_jwt): }, "last_updated": "2023-08-02T09:35:58.295052", "links": { - "bid": f"/bids/another-bid-id", - "self": f"/bids/another-bid-id/questions/bef5c1fb-46b4-4707-868a-c7719cfcc5ec", + "bid": "/bids/another-bid-id", + "self": "/bids/another-bid-id/questions/bef5c1fb-46b4-4707-868a-c7719cfcc5ec", }, "out_of": None, "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", diff --git a/tests/test_question_schema.py b/tests/test_question_schema.py index a014f73..4c7ae22 100644 --- a/tests/test_question_schema.py +++ b/tests/test_question_schema.py @@ -60,7 +60,7 @@ def test_validate_description(): # Case 3: Field validation - question_url -def test_validate_description(): +def test_validate_question_url(): data = { "description": "This is a question", "question_url": "Not a valid url", From 9c701f51860d9afd050402f01e6b6371831fc405 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 4 Aug 2023 14:46:53 +0100 Subject: [PATCH 160/208] docs: reordered steps as some make targets will be unavailable before build/run --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8be0ec8..9bdf417 100644 --- a/README.md +++ b/README.md @@ -31,20 +31,19 @@ This API stores and serves information about Methods bids for client tenders. ```bash brew install make ``` -5. Run the following command to have all the commands to use the API with Makefile: - - ```bash - make help - ``` -6. Run the following command to start the API: +5. Run the following command to start the API: ```bash make run ``` * The API will be available at http://localhost:8080/api/bids -7. Follow this link to go to the authorization documentation: [Authorization Documentation](https://github.com/methods/tdse-accessForce-auth-stub/blob/main/README.md) +6. To see all available Make targets, run the following command in a new terminal: + ```bash + make help + ``` +7. Follow this link to go to the authorization documentation: [Authorization Documentation](https://github.com/methods/tdse-accessForce-auth-stub/blob/main/README.md) 8. In a new terminal enter the following command to run authorization server if not already running. This will be needed to generate a token: From 4d109dbdded2c4cf37b18dc37bebb23f03c2d698 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 4 Aug 2023 15:06:26 +0100 Subject: [PATCH 161/208] test: added skip marker to integration test in progress --- tests/integration/test_get_bids_with_apikey.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/test_get_bids_with_apikey.py b/tests/integration/test_get_bids_with_apikey.py index 110c537..145ca1e 100644 --- a/tests/integration/test_get_bids_with_apikey.py +++ b/tests/integration/test_get_bids_with_apikey.py @@ -1,6 +1,8 @@ +import pytest import requests +@pytest.mark.skip def test_get_bids_with_api_key(): auth_response = requests.get("http://localhost:5000/authorise/") From 75161f0b0d17b882e0b974679bf5b6eac95b9f95 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Fri, 4 Aug 2023 15:10:07 +0100 Subject: [PATCH 162/208] fix: C0103: doesn't conform to snake_case naming style (invalid-name) --- .pylintrc | 36 +++++++++++++++++++++++++- api/controllers/bid_controller.py | 28 ++++++++++---------- api/controllers/question_controller.py | 24 ++++++++--------- docs/conf.py | 10 +++---- helpers/helpers.py | 14 +++++----- scripts/get_jwt.py | 32 +++++++++++------------ 6 files changed, 89 insertions(+), 55 deletions(-) diff --git a/.pylintrc b/.pylintrc index c3a2132..244fad8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,4 +4,38 @@ max-line-length=120 [MESSAGES CONTROL] -disable=C0103,W0613,R0801,C0116,W0718,R0903,R0913,R0902,W0622 \ No newline at end of file +disable= + C0103, + ; C0116, + C0114, + C0115, + C0411, + R0801, + R0902, + R0903, + R0913, + R0914, + R1710, + W0102, + W0613, + W0621, + W0622, + W0718, + W3101 +# C0103: doesn't conform to snake_case naming style (invalid-name)<- missing just helper functions +# C0116: Missing function or method docstring +# C0114: Missing module docstring +# C0115: Missing class docstring +# C0411: Wrong import order %s +# R0801: Similar lines in %s files +# R0902: Too many instance attributes +# R0903: Too few public methods +# R0913: Too many arguments +# R0914: Too many local variables +# R1710: Either all return statements in a function should return an expression, or none of them should. +# W0102: Dangerous default value %s as argument +# W0613: Unused argument +# W0621: Redefining name %r from outer scope (line %s) +# W0622: Redefining built-in 'foo' (redefined-builtin) +# W0718: Catching too general exception Exception +# W3101: Missing timmeout can expose you to a DOS attack \ No newline at end of file diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 1583b35..f1b3592 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -46,8 +46,8 @@ def post_bid(): db["bids"].insert_one(data) return data, 201 # Return 400 response if input validation fails - except ValidationError as e: - return showValidationError(e), 400 + except ValidationError as error: + return showValidationError(error), 400 # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 @@ -70,8 +70,8 @@ def get_bid_by_id(bid_id): data = prepend_host_to_links(data, hostname) return data, 200 # Return 400 if bid_id is invalid - except ValidationError as e: - return showValidationError(e), 400 + except ValidationError as error: + return showValidationError(error), 400 # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 @@ -96,10 +96,10 @@ def update_bid_by_id(bid_id): ) 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 + except ValidationError as error: + return showValidationError(error), 400 + except UnprocessableEntity as error: + return showUnprocessableEntityError(error), 422 # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 @@ -123,8 +123,8 @@ def change_status_to_deleted(bid_id): return showNotFoundError(), 404 return data, 204 # Return 400 response if input validation fails - except ValidationError as e: - return showValidationError(e), 400 + except ValidationError as error: + return showValidationError(error), 400 # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 @@ -147,10 +147,10 @@ def update_bid_status(bid_id): ) 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 + except ValidationError as error: + return showValidationError(error), 400 + except UnprocessableEntity as error: + return showUnprocessableEntityError(error), 422 # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 4d16bce..7d327be 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -34,8 +34,8 @@ def post_question(bid_id): db["questions"].insert_one(data) return data, 201 # Return 400 response if input validation fails - except ValidationError as e: - return showValidationError(e), 400 + except ValidationError as error: + return showValidationError(error), 400 # Return 500 response in case of connection failure except Exception: return showInternalServerError(), 500 @@ -60,8 +60,8 @@ def get_questions(bid_id): for question in data: prepend_host_to_links(question, hostname) return {"total_count": len(data), "items": data}, 200 - except ValidationError as e: - return showValidationError(e), 400 + except ValidationError as error: + return showValidationError(error), 400 except Exception: return showInternalServerError(), 500 @@ -84,8 +84,8 @@ def get_question(bid_id, question_id): return showNotFoundError(), 404 prepend_host_to_links(data, hostname) return data, 200 - except ValidationError as e: - return showValidationError(e), 400 + except ValidationError as error: + return showValidationError(error), 400 except Exception: return showInternalServerError(), 500 @@ -101,8 +101,8 @@ def delete_question(bid_id, question_id): return showNotFoundError(), 404 data = db["questions"].delete_one({"_id": question_id}) return data.raw_result, 204 - except ValidationError as e: - return showValidationError(e), 400 + except ValidationError as error: + return showValidationError(error), 400 except Exception: return showInternalServerError(), 500 @@ -127,9 +127,9 @@ def update_question(bid_id, question_id): updated_question = validate_question_update(request.get_json(), data) db["questions"].replace_one({"_id": question_id}, updated_question) return updated_question, 200 - except ValidationError as e: - return showValidationError(e), 400 - except UnprocessableEntity as e: - return showUnprocessableEntityError(e), 422 + except ValidationError as error: + return showValidationError(error), 400 + except UnprocessableEntity as error: + return showUnprocessableEntityError(error), 422 except Exception: return showInternalServerError(), 500 diff --git a/docs/conf.py b/docs/conf.py index 5c4e112..716cb51 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,10 +10,10 @@ sys.path.insert(0, os.path.abspath("..")) -project = "BidsApi Documentation" -copyright = "2023, Julio - Pira" -author = "Julio - Pira" -release = "0.4.0" +PROJECT = "BidsApi Documentation" +COPYRIGHT = "2023, Julio - Pira" +AUTHOR = "Julio - Pira" +RELEASE = "0.4.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -27,5 +27,5 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = "sphinx_rtd_theme" +HTML_THEME = "sphinx_rtd_theme" html_static_path = ["_static"] diff --git a/helpers/helpers.py b/helpers/helpers.py index 308da94..f340951 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -28,12 +28,12 @@ def showUnauthorizedError(): return jsonify({"Error": "Unauthorized"}) -def showUnprocessableEntityError(e): - return jsonify({"Error": str(e.description)}) +def showUnprocessableEntityError(error): + return jsonify({"Error": str(error.description)}) -def showValidationError(e): - return jsonify({"Error": str(e)}) +def showValidationError(error): + return jsonify({"Error": str(error)}) def is_valid_uuid(string): @@ -132,11 +132,11 @@ def wrapper(*args, **kwargs): def validate_token(request): - PREFIX = "Bearer " + prefix = "Bearer " auth_header = request.headers.get("Authorization") assert auth_header is not None - assert auth_header.startswith(PREFIX) is True - token = auth_header[len(PREFIX) :] + assert auth_header.startswith(prefix) is True + token = auth_header[len(prefix) :] load_dotenv() key = os.getenv("SECRET_KEY") decoded = jwt.decode(token, key, algorithms="HS256") diff --git a/scripts/get_jwt.py b/scripts/get_jwt.py index 10a9ee8..f8960dc 100644 --- a/scripts/get_jwt.py +++ b/scripts/get_jwt.py @@ -11,11 +11,11 @@ def simulate_login(username): token = token_data["jwt"] return token - except requests.exceptions.RequestException as e: - print("Request Error:", e) + except requests.exceptions.RequestException as error: + print("Request Error:", error) return None - except ValueError as e: - print("Value Error:", e) + except ValueError as error: + print("Value Error:", error) return None @@ -39,8 +39,8 @@ def post_bid(jwt_token): data = response.json() print("Post Success (id):", data["_id"]) - except requests.exceptions.RequestException as e: - print("Request Error:", e) + except requests.exceptions.RequestException as error: + print("Request Error:", error) return None @@ -54,14 +54,14 @@ def delete_bid(jwt_token): response.raise_for_status() # Raise an exception for any HTTP errors print(f"Bid with ID {bid_id} deleted successfully.") - except requests.exceptions.RequestException as e: - print("Request Error:", e) + except requests.exceptions.RequestException as error: + print("Request Error:", error) def find_bid_by_id(): try: - API_KEY = input("Enter the API key (THIS_IS_THE_API_KEY): ") - headers = {"X-API-Key": API_KEY} + api_key = input("Enter the API key (THIS_IS_THE_API_KEY): ") + headers = {"X-API-Key": api_key} bid_id = input("Enter the bid ID to find: ") response = requests.get( f"http://localhost:8080/api/bids/{bid_id}", headers=headers @@ -71,22 +71,22 @@ def find_bid_by_id(): bid_data = response.json() print("Bid Data:") print(bid_data) - except requests.exceptions.RequestException as e: - print("Request Error:", e) + except requests.exceptions.RequestException as error: + print("Request Error:", error) def find_all_bids(): try: - API_KEY = input("Enter the API key (THIS_IS_THE_API_KEY): ") - headers = {"X-API-Key": API_KEY} + api_key = input("Enter the API key (THIS_IS_THE_API_KEY): ") + headers = {"X-API-Key": api_key} response = requests.get("http://localhost:8080/api/bids", headers=headers) response.raise_for_status() # Raise an exception for any HTTP errors all_bids_data = response.json() print("All Bids Data:") print(all_bids_data) - except requests.exceptions.RequestException as e: - print("Request Error:", e) + except requests.exceptions.RequestException as error: + print("Request Error:", error) def access_level(admin): From 926ba4500185cc08c41d494243b68c8c477716f5 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Fri, 4 Aug 2023 15:22:05 +0100 Subject: [PATCH 163/208] fix: C0114: Missing module docstring (missing-module-docstring) --- .pylintrc | 6 +++--- api/controllers/bid_controller.py | 4 ++++ api/controllers/question_controller.py | 3 +++ api/models/bid_model.py | 3 +++ api/models/question_model.py | 3 +++ api/models/status_enum.py | 3 +++ api/schemas/bid_links_schema.py | 4 ++++ api/schemas/bid_schema.py | 4 ++++ api/schemas/feedback_schema.py | 4 ++++ api/schemas/id_schema.py | 3 +++ api/schemas/phase_schema.py | 3 +++ api/schemas/question_links_schema.py | 3 +++ api/schemas/question_schema.py | 3 +++ docs/conf.py | 1 + helpers/helpers.py | 3 +++ scripts/get_jwt.py | 4 ++++ tests/conftest.py | 4 ++++ tests/integration_tests/get_bids_apikey.py | 3 +++ tests/integration_tests/post_and_delete_admin.py | 4 ++++ tests/integration_tests/post_bid_jwt.py | 4 ++++ tests/test_bid_schema.py | 3 +++ tests/test_delete_bid.py | 3 +++ tests/test_delete_question.py | 3 +++ tests/test_get_bid_by_id.py | 4 ++++ tests/test_get_bids.py | 4 ++++ tests/test_get_question_by_id.py | 3 +++ tests/test_get_questions.py | 3 +++ tests/test_helpers.py | 3 +++ tests/test_phase_schema.py | 3 +++ tests/test_post_bid.py | 3 +++ tests/test_post_question.py | 3 +++ tests/test_question_schema.py | 3 +++ tests/test_update_bid_by_id.py | 3 +++ tests/test_update_bid_status.py | 3 +++ tests/test_update_question.py | 3 +++ 35 files changed, 113 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index 244fad8..6fef217 100644 --- a/.pylintrc +++ b/.pylintrc @@ -6,8 +6,8 @@ max-line-length=120 [MESSAGES CONTROL] disable= C0103, - ; C0116, - C0114, + C0116, + ; C0114, C0115, C0411, R0801, @@ -24,7 +24,7 @@ disable= W3101 # C0103: doesn't conform to snake_case naming style (invalid-name)<- missing just helper functions # C0116: Missing function or method docstring -# C0114: Missing module docstring +# C0114: Missing module docstring (missing-module-docstring) # C0115: Missing class docstring # C0411: Wrong import order %s # R0801: Similar lines in %s files diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index f1b3592..2d567eb 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,3 +1,7 @@ +""" +This module implements the bid controller. +""" + from datetime import datetime from flask import Blueprint, request from marshmallow import ValidationError diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 7d327be..d2566ac 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -1,3 +1,6 @@ +""" +This module implements the Question Controller blueprint. +""" from flask import Blueprint, request from marshmallow import ValidationError from werkzeug.exceptions import UnprocessableEntity diff --git a/api/models/bid_model.py b/api/models/bid_model.py index f0b4704..cbc3542 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -1,3 +1,6 @@ +""" +This module contains the data model for the bid resource. +""" from uuid import uuid4 from datetime import datetime from api.models.status_enum import Status diff --git a/api/models/question_model.py b/api/models/question_model.py index f7dc480..89c3038 100644 --- a/api/models/question_model.py +++ b/api/models/question_model.py @@ -1,3 +1,6 @@ +""" +This module contains the data model for the question resource. +""" from uuid import uuid4 from datetime import datetime from api.models.status_enum import Status diff --git a/api/models/status_enum.py b/api/models/status_enum.py index 54e88fa..402d910 100644 --- a/api/models/status_enum.py +++ b/api/models/status_enum.py @@ -1,3 +1,6 @@ +""" +This module contains the Status enum. +""" from enum import Enum, unique diff --git a/api/schemas/bid_links_schema.py b/api/schemas/bid_links_schema.py index 78d23da..78ea041 100644 --- a/api/schemas/bid_links_schema.py +++ b/api/schemas/bid_links_schema.py @@ -1,3 +1,7 @@ +""" +This module contains the schema for representing links in a bid resource. +""" + from marshmallow import Schema, fields diff --git a/api/schemas/bid_schema.py b/api/schemas/bid_schema.py index 6d21806..d61981f 100644 --- a/api/schemas/bid_schema.py +++ b/api/schemas/bid_schema.py @@ -1,3 +1,7 @@ +""" +This module contains the BidSchema class, which is a Marshmallow schema for the bid object. +""" + from marshmallow import Schema, fields, post_load, validates_schema, ValidationError from api.models.status_enum import Status from api.models.bid_model import BidModel diff --git a/api/schemas/feedback_schema.py b/api/schemas/feedback_schema.py index 1c1809c..459dd47 100644 --- a/api/schemas/feedback_schema.py +++ b/api/schemas/feedback_schema.py @@ -1,3 +1,7 @@ +""" +This module contains the schema for the feedback data in a bid. +""" + from marshmallow import Schema, fields diff --git a/api/schemas/id_schema.py b/api/schemas/id_schema.py index 98f9eab..712ecac 100644 --- a/api/schemas/id_schema.py +++ b/api/schemas/id_schema.py @@ -1,3 +1,6 @@ +""" +This module contains the schema for validating path param IDs. +""" from marshmallow import Schema, fields, validate diff --git a/api/schemas/phase_schema.py b/api/schemas/phase_schema.py index 8bcaaa8..9c7a1dd 100644 --- a/api/schemas/phase_schema.py +++ b/api/schemas/phase_schema.py @@ -1,3 +1,6 @@ +""" +This module contains the schema for representing a bid phase. +""" from enum import Enum, unique from marshmallow import Schema, fields, validates_schema, ValidationError diff --git a/api/schemas/question_links_schema.py b/api/schemas/question_links_schema.py index aa56bfe..dc9c747 100644 --- a/api/schemas/question_links_schema.py +++ b/api/schemas/question_links_schema.py @@ -1,3 +1,6 @@ +""" +This module contains the schema for representing links in a question resource. +""" from marshmallow import Schema, fields diff --git a/api/schemas/question_schema.py b/api/schemas/question_schema.py index e0cf85f..5e20203 100644 --- a/api/schemas/question_schema.py +++ b/api/schemas/question_schema.py @@ -1,3 +1,6 @@ +""" +This module contains the marshmallow schema for the question resource. +""" from marshmallow import Schema, fields, post_load from api.models.status_enum import Status from api.models.question_model import QuestionModel diff --git a/docs/conf.py b/docs/conf.py index 716cb51..486cc4d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,4 @@ +"""Sphinx configuration""" # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: diff --git a/helpers/helpers.py b/helpers/helpers.py index f340951..6bfa612 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,3 +1,6 @@ +""" +This module contains helper functions for the API. +""" import jwt from jwt.exceptions import InvalidTokenError import os diff --git a/scripts/get_jwt.py b/scripts/get_jwt.py index f8960dc..9493d58 100644 --- a/scripts/get_jwt.py +++ b/scripts/get_jwt.py @@ -1,3 +1,7 @@ +""" +This script simulates a login to the API and then allows the user to perform +""" + import requests diff --git a/tests/conftest.py b/tests/conftest.py index ae6611d..f483d1f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,7 @@ +""" +This file contains fixtures that are used by multiple tests. +""" + import jwt import pytest import os diff --git a/tests/integration_tests/get_bids_apikey.py b/tests/integration_tests/get_bids_apikey.py index 1160c79..637fa1a 100644 --- a/tests/integration_tests/get_bids_apikey.py +++ b/tests/integration_tests/get_bids_apikey.py @@ -1,3 +1,6 @@ +""" +This script is used to test the get bids endpoint with an API key. +""" import requests response = requests.get("http://localhost:5000/authorise/") diff --git a/tests/integration_tests/post_and_delete_admin.py b/tests/integration_tests/post_and_delete_admin.py index 25a9472..881f0a7 100644 --- a/tests/integration_tests/post_and_delete_admin.py +++ b/tests/integration_tests/post_and_delete_admin.py @@ -1,3 +1,7 @@ +""" +This script is used to test the post and delete functionality of the API. +""" + import json import requests diff --git a/tests/integration_tests/post_bid_jwt.py b/tests/integration_tests/post_bid_jwt.py index 29e3f7e..610412c 100644 --- a/tests/integration_tests/post_bid_jwt.py +++ b/tests/integration_tests/post_bid_jwt.py @@ -1,3 +1,7 @@ +""" +This script is used to test the API endpoint for posting a bid. +""" + import json import requests diff --git a/tests/test_bid_schema.py b/tests/test_bid_schema.py index fd29091..59eeda0 100644 --- a/tests/test_bid_schema.py +++ b/tests/test_bid_schema.py @@ -1,3 +1,6 @@ +""" +This module contains tests for the bid schema. +""" import pytest from marshmallow import ValidationError from api.schemas.bid_schema import BidSchema diff --git a/tests/test_delete_bid.py b/tests/test_delete_bid.py index af29a7b..ab66e8e 100644 --- a/tests/test_delete_bid.py +++ b/tests/test_delete_bid.py @@ -1,3 +1,6 @@ +""" +This file contains the tests for the DELETE /api/bids/ endpoint +""" from unittest.mock import patch diff --git a/tests/test_delete_question.py b/tests/test_delete_question.py index 14b1dd0..7f43135 100644 --- a/tests/test_delete_question.py +++ b/tests/test_delete_question.py @@ -1,3 +1,6 @@ +""" +This file contains the tests for the delete_question endpoint +""" from unittest.mock import patch diff --git a/tests/test_get_bid_by_id.py b/tests/test_get_bid_by_id.py index 71b7b81..c97ce05 100644 --- a/tests/test_get_bid_by_id.py +++ b/tests/test_get_bid_by_id.py @@ -1,3 +1,7 @@ +""" +This file contains the tests for the get_bid_by_id endpoint. +""" + from unittest.mock import patch diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 58cacd8..0d3d73e 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -1,3 +1,7 @@ +""" +This file contains tests for the GET /bids endpoint. +""" + from unittest.mock import patch diff --git a/tests/test_get_question_by_id.py b/tests/test_get_question_by_id.py index 8947845..5221321 100644 --- a/tests/test_get_question_by_id.py +++ b/tests/test_get_question_by_id.py @@ -1,3 +1,6 @@ +""" +This file contains the tests for the GET /bids/{bidId}/questions/{questionId} endpoint +""" from unittest.mock import patch diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index 41069a2..7ef9adc 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -1,3 +1,6 @@ +""" +This file contains tests for the GET /bids/{bid_id}/questions endpoint. +""" from unittest.mock import patch diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 39d2106..7b315ed 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,3 +1,6 @@ +""" +This file contains tests for the helper functions in helpers.py +""" from helpers.helpers import prepend_host_to_links diff --git a/tests/test_phase_schema.py b/tests/test_phase_schema.py index 55ead69..468882b 100644 --- a/tests/test_phase_schema.py +++ b/tests/test_phase_schema.py @@ -1,3 +1,6 @@ +""" +This file contains tests for the phase schema. +""" from unittest.mock import patch diff --git a/tests/test_post_bid.py b/tests/test_post_bid.py index 80e9b2d..225dd3d 100644 --- a/tests/test_post_bid.py +++ b/tests/test_post_bid.py @@ -1,3 +1,6 @@ +""" +This file contains the tests for the POST /api/bids endpoint +""" from unittest.mock import patch diff --git a/tests/test_post_question.py b/tests/test_post_question.py index ffd6aa9..3707b1a 100644 --- a/tests/test_post_question.py +++ b/tests/test_post_question.py @@ -1,3 +1,6 @@ +""" +This file contains the tests for the POST /bids//questions endpoint +""" from unittest.mock import patch diff --git a/tests/test_question_schema.py b/tests/test_question_schema.py index 4c7ae22..ee4088b 100644 --- a/tests/test_question_schema.py +++ b/tests/test_question_schema.py @@ -1,3 +1,6 @@ +""" +This module contains the schema for validating question data. +""" import pytest from marshmallow import ValidationError from api.schemas.question_schema import QuestionSchema diff --git a/tests/test_update_bid_by_id.py b/tests/test_update_bid_by_id.py index d45d16f..d62d768 100644 --- a/tests/test_update_bid_by_id.py +++ b/tests/test_update_bid_by_id.py @@ -1,3 +1,6 @@ +""" +This file contains tests for the update_bid_by_id endpoint +""" from unittest.mock import patch diff --git a/tests/test_update_bid_status.py b/tests/test_update_bid_status.py index 75dca14..2608897 100644 --- a/tests/test_update_bid_status.py +++ b/tests/test_update_bid_status.py @@ -1,3 +1,6 @@ +""" +This file contains tests for the update_bid_status endpoint. +""" from unittest.mock import patch diff --git a/tests/test_update_question.py b/tests/test_update_question.py index 8b36077..3e4ac4e 100644 --- a/tests/test_update_question.py +++ b/tests/test_update_question.py @@ -1,3 +1,6 @@ +""" +This file contains tests for the update_question endpoint. +""" from unittest.mock import patch From 23cabcc68afb1753935933c9d9448f98222360ec Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Fri, 4 Aug 2023 15:25:10 +0100 Subject: [PATCH 164/208] fix: C0411: Wrong import order %s --- .pylintrc | 2 +- helpers/helpers.py | 8 ++++---- tests/conftest.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6fef217..379c507 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,7 +9,7 @@ disable= C0116, ; C0114, C0115, - C0411, + ; C0411, R0801, R0902, R0903, diff --git a/helpers/helpers.py b/helpers/helpers.py index 6bfa612..085b4f3 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,14 +1,14 @@ """ This module contains helper functions for the API. """ -import jwt -from jwt.exceptions import InvalidTokenError import os import uuid -from dotenv import load_dotenv +from functools import wraps from datetime import datetime +import jwt +from jwt.exceptions import InvalidTokenError +from dotenv import load_dotenv from flask import jsonify, request -from functools import wraps from werkzeug.exceptions import UnprocessableEntity from api.schemas.bid_schema import BidSchema from api.schemas.id_schema import IdSchema diff --git a/tests/conftest.py b/tests/conftest.py index f483d1f..d99b26a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,9 @@ This file contains fixtures that are used by multiple tests. """ +import os import jwt import pytest -import os from dotenv import load_dotenv from flask import Flask from api.controllers.bid_controller import bid From 7246de6096a578a215c49f84f954c2c6a566360c Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Fri, 4 Aug 2023 16:16:04 +0100 Subject: [PATCH 165/208] fix: merge conflicts --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0c38f74..51e8f86 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,6 @@ import os import jwt import pytest -import os from app import app from dotenv import load_dotenv From 141a21378c9ffce7670bf2c780e7462b6c8faa39 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 7 Aug 2023 17:52:34 +0100 Subject: [PATCH 166/208] feat: updated swagger; created pagination validation helper function; implemented skip and limit on get_questions --- api/controllers/question_controller.py | 38 ++++++++++++++++++++------ helpers/helpers.py | 22 +++++++++++++++ static/swagger_config.yml | 33 ++++++++++++++++++++-- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 79240a5..5ada438 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -1,7 +1,7 @@ """ This module implements the Question Controller blueprint. """ -from flask import Blueprint, request +from flask import Blueprint, request, jsonify from marshmallow import ValidationError from werkzeug.exceptions import UnprocessableEntity from api.models.status_enum import Status @@ -17,6 +17,7 @@ prepend_host_to_links, require_jwt, require_admin_access, + validate_pagination, ) question = Blueprint("question", __name__) @@ -50,23 +51,44 @@ def get_questions(bid_id): try: bid_id = validate_id_path(bid_id) hostname = request.headers.get("host") + valid_limit_offset = validate_pagination( + request.args.get("limit"), request.args.get("offset") + ) + limit = valid_limit_offset[0] + offset = valid_limit_offset[1] data = list( - db["questions"].find( + db["questions"] + .find( { "status": {"$ne": Status.DELETED.value}, - "links.bid": f"/bids/{bid_id}", + "links.bid": f"/api/bids/{bid_id}", } ) + .skip(offset) + .limit(limit) ) if len(data) == 0: return showNotFoundError(), 404 for question in data: prepend_host_to_links(question, hostname) - return {"total_count": len(data), "items": data}, 200 + return { + "total_count": db["questions"].count_documents( + { + "status": {"$ne": Status.DELETED.value}, + "links.bid": f"/api/bids/{bid_id}", + } + ), + "count": len(data), + "offset": offset, + "limit": limit, + "items": data, + }, 200 except ValidationError as error: return showValidationError(error), 400 - except Exception: - return showInternalServerError(), 500 + except ValueError as error: + return jsonify({"Error": str(error)}), 400 + except Exception as e: + return str(e), 500 @question.route("/bids//questions/", methods=["GET"]) @@ -79,7 +101,7 @@ def get_question(bid_id, question_id): data = db["questions"].find_one( { "_id": question_id, - "links.self": f"/bids/{bid_id}/questions/{question_id}", + "links.self": f"/api/bids/{bid_id}/questions/{question_id}", "status": {"$ne": Status.DELETED.value}, } ) @@ -119,7 +141,7 @@ def update_question(bid_id, question_id): data = db["questions"].find_one( { "_id": question_id, - "links.self": f"/bids/{bid_id}/questions/{question_id}", + "links.self": f"/api/bids/{bid_id}/questions/{question_id}", } ) # Return 404 response if not found / returns None diff --git a/helpers/helpers.py b/helpers/helpers.py index 085b4f3..c230e3b 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -162,3 +162,25 @@ def validate_question_update(request, resource): question_document = QuestionSchema().load(resource, partial=True) data = QuestionSchema().dump(question_document) return data + + +def validate_pagination(limit, offset): + load_dotenv() + + def validate_param(value, default_value, max_value, param_name): + maximum = int(os.getenv(max_value)) + if value is not None: + try: + valid_value = int(value) + assert maximum > valid_value >= 0 + except (ValueError, AssertionError): + raise ValueError( + f"{param_name} value must be a positive integer less than {maximum}" + ) + else: + valid_value = int(os.getenv(default_value)) + return valid_value + + valid_limit = validate_param(limit, "DEFAULT_LIMIT", "MAX_LIMIT", "Limit") + valid_offset = validate_param(offset, "DEFAULT_OFFSET", "MAX_OFFSET", "Offset") + return valid_limit, valid_offset diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 87b8268..582afe1 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -45,8 +45,23 @@ paths: get: tags: - bids - summary: Returns all bids + summary: Returns a list of bids description: A JSON with item count and array of all bids + parameters: + - name: limit + in: query + description: number of results to return per page + required: false + schema: + type: string + format: integer + - name: offset + in: query + description: number of items skipped from start of dataset on current page + required: false + schema: + type: string + format: integer security: - ApiKeyAuth: [] responses: @@ -244,7 +259,7 @@ paths: get: tags: - questions - summary: Returns all questions for a bid + summary: Returns a list of questions for a bid description: A JSON with item count and array of all questions for a bid operationId: get_questions parameters: @@ -255,6 +270,20 @@ paths: schema: type: string format: uuid + - name: limit + in: query + description: number of results to return per page + required: false + schema: + type: string + format: integer + - name: offset + in: query + description: number of items skipped from start of dataset on current page + required: false + schema: + type: string + format: integer security: - BearerAuth: [] responses: From a4ad121751f3f7563673d0cf364dcef540785485 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 8 Aug 2023 14:24:27 +0100 Subject: [PATCH 167/208] test: tested validate_pagination; wip - mocking chained db call with limit and skip --- api/controllers/question_controller.py | 21 ++++++------ helpers/helpers.py | 2 +- tests/test_get_questions.py | 45 ++++++-------------------- tests/test_helpers.py | 40 ++++++++++++++++++++--- 4 files changed, 55 insertions(+), 53 deletions(-) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 5ada438..4637889 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -51,11 +51,9 @@ def get_questions(bid_id): try: bid_id = validate_id_path(bid_id) hostname = request.headers.get("host") - valid_limit_offset = validate_pagination( + limit, offset = validate_pagination( request.args.get("limit"), request.args.get("offset") ) - limit = valid_limit_offset[0] - offset = valid_limit_offset[1] data = list( db["questions"] .find( @@ -67,17 +65,18 @@ def get_questions(bid_id): .skip(offset) .limit(limit) ) + total_count = db["questions"].count_documents( + { + "status": {"$ne": Status.DELETED.value}, + "links.bid": f"/api/bids/{bid_id}", + } + ) if len(data) == 0: return showNotFoundError(), 404 for question in data: prepend_host_to_links(question, hostname) return { - "total_count": db["questions"].count_documents( - { - "status": {"$ne": Status.DELETED.value}, - "links.bid": f"/api/bids/{bid_id}", - } - ), + "total_count": total_count, "count": len(data), "offset": offset, "limit": limit, @@ -87,8 +86,8 @@ def get_questions(bid_id): return showValidationError(error), 400 except ValueError as error: return jsonify({"Error": str(error)}), 400 - except Exception as e: - return str(e), 500 + except Exception: + return showInternalServerError(), 500 @question.route("/bids//questions/", methods=["GET"]) diff --git a/helpers/helpers.py b/helpers/helpers.py index c230e3b..8f08a2f 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -175,7 +175,7 @@ def validate_param(value, default_value, max_value, param_name): assert maximum > valid_value >= 0 except (ValueError, AssertionError): raise ValueError( - f"{param_name} value must be a positive integer less than {maximum}" + f"{param_name} value must be a number between 0 and {maximum}" ) else: valid_value = int(os.getenv(default_value)) diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index 7ef9adc..ca11ae7 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -19,27 +19,8 @@ def test_get_questions_success(mock_db, test_client, basic_jwt): }, "last_updated": "2023-08-01T23:11:59.336092", "links": { - "bid": f"/bids/{sample_bid_id}", - "self": f"/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", - }, - "out_of": None, - "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "respondents": [], - "response": None, - "score": None, - "status": "in_progress", - }, - { - "_id": "bef5c1fb-46b4-4707-868a-c7719cfcc5ec", - "description": "This is a question", - "feedback": { - "description": "Good feedback", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder", - }, - "last_updated": "2023-08-02T09:35:58.295052", - "links": { - "bid": "/bids/another-bid-id", - "self": "/bids/another-bid-id/questions/bef5c1fb-46b4-4707-868a-c7719cfcc5ec", + "bid": f"/api/bids/{sample_bid_id}", + "self": f"/api/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", }, "out_of": None, "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", @@ -50,15 +31,7 @@ def test_get_questions_success(mock_db, test_client, basic_jwt): }, ] - # Filter the sample data to include only questions with the desired bid link - filtered_sample_data = [ - question - for question in sample_data - if question["links"]["bid"] == f"/bids/{sample_bid_id}" - ] - - # Mock the database find method to return the filtered sample data - mock_db["questions"].find.return_value = filtered_sample_data + mock_db["questions"].find.return_value = sample_data # Make a request to the endpoint to get the questions response = test_client.get( @@ -69,8 +42,8 @@ def test_get_questions_success(mock_db, test_client, basic_jwt): # Assert the response status code and content assert response.status_code == 200 response_data = response.get_json() - assert response_data["total_count"] == len(filtered_sample_data) - assert response_data["items"] == filtered_sample_data + assert response_data["total_count"] == len(sample_data) + assert response_data["items"] == sample_data # Case 2: Links prepended with hostname @@ -88,8 +61,8 @@ def test_links_with_host(mock_db, test_client, basic_jwt): }, "last_updated": "2023-08-01T23:11:59.336092", "links": { - "bid": f"/bids/{sample_bid_id}", - "self": f"/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", + "bid": f"/api/bids/{sample_bid_id}", + "self": f"/api/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", }, "out_of": None, "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", @@ -113,12 +86,12 @@ def test_links_with_host(mock_db, test_client, basic_jwt): response_data = response.get_json() assert ( response_data["items"][0]["links"]["bid"] - == f"http://localhost:8080/bids/{sample_bid_id}" + == f"http://localhost:8080/api/bids/{sample_bid_id}" ) assert ( response_data["items"][0]["links"]["self"] - == f"http://localhost:8080/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791" + == f"http://localhost:8080/api/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791" ) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 7b315ed..5a8ce56 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,7 +1,8 @@ """ This file contains tests for the helper functions in helpers.py """ -from helpers.helpers import prepend_host_to_links +import pytest +from helpers.helpers import prepend_host_to_links, validate_pagination # Case 1: Host is prepended to values in links object @@ -14,8 +15,8 @@ def test_prepend_host(): "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", + "questions": "/api/bids/9f688442-b535-4683-ae1a-a64c1a3b8616/questions", + "self": "/api/bids/9f688442-b535-4683-ae1a-a64c1a3b8616", }, "status": "in_progress", "tender": "Business Intelligence and Data Warehousing", @@ -26,6 +27,35 @@ def test_prepend_host(): 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/api/bids/9f688442-b535-4683-ae1a-a64c1a3b8616/questions", + "self": "http://localhost:8080/api/bids/9f688442-b535-4683-ae1a-a64c1a3b8616", } + + +def test_validate_pagination(): + valid_limit = 10 + valid_offset = 20 + nan_limit = "five" + nan_offset = "ten" + negative_limit = -5 + negative_offset = -10 + + assert validate_pagination(valid_limit, valid_offset) == (10, 20) + assert validate_pagination(None, valid_offset) == (20, 20) + assert validate_pagination(valid_limit, None) == (10, 0) + with pytest.raises( + ValueError, match="Limit value must be a number between 0 and 1000" + ): + validate_pagination(nan_limit, valid_offset) + with pytest.raises( + ValueError, match="Limit value must be a number between 0 and 1000" + ): + validate_pagination(negative_limit, valid_offset) + with pytest.raises( + ValueError, match="Offset value must be a number between 0 and 2000" + ): + validate_pagination(valid_limit, nan_offset) + with pytest.raises( + ValueError, match="Offset value must be a number between 0 and 2000" + ): + validate_pagination(valid_limit, negative_offset) From 30c8be1514a5738bc60905b7a8c8d091a5bbcbff Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Tue, 8 Aug 2023 15:40:56 +0100 Subject: [PATCH 168/208] refactor: get bids with sort --- api/controllers/bid_controller.py | 47 ++++++++++++++++++++++++++---- request_examples/get_all_bids.http | 2 +- request_examples/post_bid.http | 6 ++-- request_examples/update_bid.http | 5 ++-- tests/test_get_bids.py | 36 ++++++++++++++++------- 5 files changed, 75 insertions(+), 21 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 2d567eb..74cd9d5 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,9 +1,8 @@ """ This module implements the bid controller. """ - from datetime import datetime -from flask import Blueprint, request +from flask import Blueprint, request, jsonify from marshmallow import ValidationError from werkzeug.exceptions import UnprocessableEntity from api.models.status_enum import Status @@ -29,13 +28,51 @@ @bid.route("/bids", methods=["GET"]) @require_api_key def get_bids(): - # Get all bids from database collection try: - data = list(db["bids"].find({"status": {"$ne": Status.DELETED.value}})) + # Validate and process the limit parameter + limit = int(request.args.get("limit", 5)) + if limit < 1: + return jsonify({"error": "Limit must be a positive number"}), 400 + + offset = int(request.args.get("offset", 0)) + if offset < 0 or offset >= 1000: + return jsonify({"error": "Offset must be between 0 and 999"}), 400 + + sort = request.args.get("sort", "last_updated") + order = int(request.args.get("order", 1)) # Default to ascending (1) + + # Prepare query filter and options + query_filter = {"status": {"$ne": Status.DELETED.value}} + query_options = {"sort": [(sort, order)], "skip": offset, "limit": limit} + + # Fetch data and count documents + data = list(db["bids"].find(query_filter, **query_options)) + total_count = db["bids"].count_documents(query_filter) + + explain_result = ( + db["bids"] + .find({"status": {"$ne": Status.DELETED.value}}) + .sort("alias", order) + .explain() + ) + print(explain_result["queryPlanner"]["winningPlan"]) + # Return the response + if not data: + return showNotFoundError(), 404 + hostname = request.headers.get("host") for resource in data: prepend_host_to_links(resource, hostname) - return {"total_count": len(data), "items": data}, 200 + + return { + "count": len(data), + "total_count": total_count, + "limit": limit, + "offset": offset, + "data": data, + }, 200 + except ValueError: # Handle non-integer values + return jsonify({"error": "Page and limit must be valid integer values"}), 400 except Exception: return showInternalServerError(), 500 diff --git a/request_examples/get_all_bids.http b/request_examples/get_all_bids.http index 51e3ef0..b419266 100644 --- a/request_examples/get_all_bids.http +++ b/request_examples/get_all_bids.http @@ -1,2 +1,2 @@ -GET http://localhost:8080/api/bids HTTP/1.1 +GET http://localhost:8080/api/bids?sort=alias&order=1 HTTP/1.1 X-API-Key: THIS_IS_THE_API_KEY \ No newline at end of file diff --git a/request_examples/post_bid.http b/request_examples/post_bid.http index 8b561e5..d7d2e29 100644 --- a/request_examples/post_bid.http +++ b/request_examples/post_bid.http @@ -3,8 +3,8 @@ Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTUyNDk0M30.GYWsLyCddSJqxFBCXJc5OMivZsQNQBUTaW6rd0bfq7A { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-06-23", + "tender": "THIS IS A TENDER", + "client": "TEST", + "bid_date": "2023-10-23", "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder" } diff --git a/request_examples/update_bid.http b/request_examples/update_bid.http index 47e08bd..8efbb88 100644 --- a/request_examples/update_bid.http +++ b/request_examples/update_bid.http @@ -1,6 +1,7 @@ -PUT http://localhost:8080/api/bids/34c765e7-ed75-4108-80ac-2c902bcf38f1/status HTTP/1.1 +PUT http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRlc3RlciBNY1Rlc3RmYWNlIiwiYWRtaW4iOmZhbHNlfQ.Dg7f8LVtALYWvjZH31re5C-Pc6Hp6Ra-U4LAy0ZQQ9M Content-Type: application/json { - "status": "completed" + "was_successful": "true" } diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 0d3d73e..25ad5e2 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -1,21 +1,35 @@ -""" -This file contains tests for the GET /bids endpoint. -""" - from unittest.mock import patch # Case 1: Successful get @patch("api.controllers.bid_controller.db") def test_get_bids_success(mock_db, test_client, api_key): - mock_db["bids"].find.return_value = [] + # Mock the find method + mock_db["bids"].find().skip().limit.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 = test_client.get( + response_data = test_client.get( "/api/bids", headers={"host": "localhost:8080", "X-API-Key": api_key} ) - mock_db["bids"].find.assert_called_once_with({"status": {"$ne": "deleted"}}) - assert response.status_code == 200 - assert response.get_json() == {"total_count": 0, "items": []} + assert response_data.status_code == 200 + + # Assert response structure + assert "count" in response_data + assert "total_count" in response_data + assert "limit" in response_data + assert "offset" in response_data + assert "data" in response_data # Case 2: Links prepended with hostname @@ -41,7 +55,9 @@ def test_links_with_host(mock_db, test_client, api_key): assert response.status_code == 200 assert response.get_json() == { "total_count": 1, - "items": [ + "limit": 5, + "offset": 0, + "data": [ { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "bid_date": "2023-06-23", From ee7dfdf64a693566971eead4e5e943dd0f2b81ff Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 8 Aug 2023 16:04:30 +0100 Subject: [PATCH 169/208] test: debugged get tests --- api/controllers/question_controller.py | 8 ++++---- tests/test_get_questions.py | 14 +++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 4637889..471436c 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -71,7 +71,7 @@ def get_questions(bid_id): "links.bid": f"/api/bids/{bid_id}", } ) - if len(data) == 0: + if not data: return showNotFoundError(), 404 for question in data: prepend_host_to_links(question, hostname) @@ -104,7 +104,7 @@ def get_question(bid_id, question_id): "status": {"$ne": Status.DELETED.value}, } ) - if len(data) == 0: + if not data: return showNotFoundError(), 404 prepend_host_to_links(data, hostname) return data, 200 @@ -121,7 +121,7 @@ def delete_question(bid_id, question_id): bid_id = validate_id_path(bid_id) question_id = validate_id_path(question_id) bid = db["bids"].find_one({"_id": bid_id}) - if bid is None: + if not bid: return showNotFoundError(), 404 data = db["questions"].delete_one({"_id": question_id}) return data.raw_result, 204 @@ -144,7 +144,7 @@ def update_question(bid_id, question_id): } ) # Return 404 response if not found / returns None - if len(data) == 0: + if not data: return showNotFoundError(), 404 updated_question = validate_question_update(request.get_json(), data) diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index ca11ae7..060ce92 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -31,7 +31,11 @@ def test_get_questions_success(mock_db, test_client, basic_jwt): }, ] - mock_db["questions"].find.return_value = sample_data + mock_db[ + "questions" + ].find.return_value.skip.return_value.limit.return_value = sample_data + + mock_db["questions"].count_documents.return_value = len(sample_data) # Make a request to the endpoint to get the questions response = test_client.get( @@ -74,7 +78,11 @@ def test_links_with_host(mock_db, test_client, basic_jwt): ] # Mock the database find method to return the filtered sample data - mock_db["questions"].find.return_value = sample_data + mock_db[ + "questions" + ].find.return_value.skip.return_value.limit.return_value = sample_data + + mock_db["questions"].count_documents.return_value = len(sample_data) # Make a request to the endpoint to get the questions response = test_client.get( @@ -154,7 +162,7 @@ def test_no_questions_found(mock_db, test_client, basic_jwt): sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" # Mock the database find method to return an empty list - mock_db["questions"].find.return_value = [] + mock_db["questions"].find.return_value.skip.return_value.limit.return_value = [] # Make a request to the endpoint to get the questions response = test_client.get( From aa8eb39022a488a2ac5f85d227923ba0c4d394c0 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 8 Aug 2023 18:26:34 +0100 Subject: [PATCH 170/208] refactor: modified script and test_data for larger dataset --- README.md | 6 +- api/controllers/question_controller.py | 7 +- helpers/helpers.py | 15 +- scripts/create_bids.py | 2 +- scripts/create_questions.py | 22 +- scripts/test_data/bids.json | 426 +++++++++++++++++++------ scripts/test_data/questions.json | 19 ++ 7 files changed, 384 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 9bdf417..8c37208 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ This API stores and serves information about Methods bids for client tenders. ## Environmental Variables -In order to validate your credentials and configure the database connection, you will have to set up the environmental variables locally. +In order to validate your credentials, configure the database connection and utilise pagination you will have to set up the environmental variables locally. To do this, create a `.env` file in your root folder, with the following key/value pairs: @@ -65,6 +65,10 @@ To do this, create a `.env` file in your root folder, with the following key/val DB_HOST=localhost DB_NAME=bidsAPI TEST_DB_NAME=testAPI + DEFAULT_OFFSET=0 + DEFAULT_LIMIT=20 + MAX_OFFSET=2000 + MAX_LIMIT=1000 -------------- diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 471436c..4029476 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -18,6 +18,7 @@ require_jwt, require_admin_access, validate_pagination, + validate_sort, ) question = Blueprint("question", __name__) @@ -51,6 +52,7 @@ def get_questions(bid_id): try: bid_id = validate_id_path(bid_id) hostname = request.headers.get("host") + field, order = validate_sort(request.args.get("sort")) limit, offset = validate_pagination( request.args.get("limit"), request.args.get("offset") ) @@ -62,6 +64,7 @@ def get_questions(bid_id): "links.bid": f"/api/bids/{bid_id}", } ) + .sort(field, order) .skip(offset) .limit(limit) ) @@ -86,8 +89,8 @@ def get_questions(bid_id): return showValidationError(error), 400 except ValueError as error: return jsonify({"Error": str(error)}), 400 - except Exception: - return showInternalServerError(), 500 + except Exception as e: + return str(e), 500 @question.route("/bids//questions/", methods=["GET"]) diff --git a/helpers/helpers.py b/helpers/helpers.py index 8f08a2f..ef22697 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -169,7 +169,7 @@ def validate_pagination(limit, offset): def validate_param(value, default_value, max_value, param_name): maximum = int(os.getenv(max_value)) - if value is not None: + if value: try: valid_value = int(value) assert maximum > valid_value >= 0 @@ -184,3 +184,16 @@ def validate_param(value, default_value, max_value, param_name): valid_limit = validate_param(limit, "DEFAULT_LIMIT", "MAX_LIMIT", "Limit") valid_offset = validate_param(offset, "DEFAULT_OFFSET", "MAX_OFFSET", "Offset") return valid_limit, valid_offset + + +def validate_sort(sort_value): + if sort_value: + if sort_value[0] == "-": + field = sort_value[1:] + order = -1 + else: + field = sort_value + order = 1 + return field, order + else: + return "description", 1 diff --git a/scripts/create_bids.py b/scripts/create_bids.py index 1217785..ce7cb64 100644 --- a/scripts/create_bids.py +++ b/scripts/create_bids.py @@ -41,7 +41,7 @@ def populate_bids(): bids_data = json.load(bids_file) # Insert bids into the database - for bid in bids_data["items"]: + for bid in bids_data: # Check if the bid already exists in the database existing_bid = collection.find_one({"_id": bid["_id"]}) if existing_bid: diff --git a/scripts/create_questions.py b/scripts/create_questions.py index aec0208..f059714 100644 --- a/scripts/create_questions.py +++ b/scripts/create_questions.py @@ -4,9 +4,11 @@ """ -import os +import copy import json +import os import sys +import uuid from itertools import zip_longest from pymongo import MongoClient from pymongo.errors import ConnectionFailure @@ -49,20 +51,20 @@ def populate_questions(): questions_data = json.load(questions_file) # Update questions data with existing bid ids from bids.json - updated_questions = [] - bids = bids_data["items"] - questions_groups = [ - questions_data[i : i + 3] for i in range(0, len(questions_data), 3) - ] + updated_questions = [] - for bid, questions in zip_longest(bids, questions_groups, fillvalue=None): + for bid in bids_data: bid_url = bid["links"]["self"] + bid_status = bid["status"] + questions = copy.deepcopy(questions_data) for question in questions: - self_url = question["links"]["self"] + question_id = uuid.uuid4() question["links"]["bid"] = bid_url - question["links"]["self"] = f"{bid_url}{self_url}" + question["links"]["self"] = f"{bid_url}/questions/{question_id}" + question["status"] = bid_status + question["_id"] = str(question_id) updated_questions.append(question) # Insert questions into the database @@ -75,6 +77,8 @@ def populate_questions(): collection.insert_one(question) print(f"Inserted question with _id: {question['_id']}") + collection.create_index("description") + except ConnectionFailure: print("Error: Failed to connect to database") sys.exit(1) diff --git a/scripts/test_data/bids.json b/scripts/test_data/bids.json index 9208699..43e26b8 100644 --- a/scripts/test_data/bids.json +++ b/scripts/test_data/bids.json @@ -1,104 +1,332 @@ -{ - "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": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + "self": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1" + }, + "status": "in_progress", + "success": [ { - "_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": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", - "self": "/api/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 - }, + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 30 + } + ], + "tender": "Business Intelligence and Data Warehousing", + "was_successful": false + }, + { + "_id": "a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7", + "alias": "ACME", + "bid_date": "2023-07-05", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "ACME Corporation", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 15 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-08-01T14:30:20.123456", + "links": { + "questions": "/api/bids/a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7/questions", + "self": "/api/bids/a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7" + }, + "status": "in_progress", + "success": [ { - "_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": "/api/bids/b4846631-9135-4208-8e37-70eba8f77e15/questions", - "self": "/api/bids/b4846631-9135-4208-8e37-70eba8f77e15" - }, - "status": "completed", - "success": [ - { - "has_score": true, - "out_of": 36, - "phase": 1, - "score": 30 - } - ], - "tender": "Business Intelligence and Data Warehousing", - "was_successful": false - }, + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 28 + } + ], + "tender": "Data Analytics Solution", + "was_successful": false + }, + { + "_id": "e2c91b49-76ac-4d23-a4e3-109d65b8ac6f", + "alias": "XYZ", + "bid_date": "2023-07-10", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "XYZ Innovations", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 18 + }, + "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": "/api/bids/e2c91b49-76ac-4d23-a4e3-109d65b8ac6f/questions", + "self": "/api/bids/e2c91b49-76ac-4d23-a4e3-109d65b8ac6f" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 32 + } + ], + "tender": "Advanced Analytics Platform", + "was_successful": false + }, + { + "_id": "f73e6c4d-9f2d-4e66-8c57-7ae8d20a558e", + "alias": "TechSolutions", + "bid_date": "2023-07-15", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Technology Solutions Ltd.", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 22 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-08-05T09:15:30.765432", + "links": { + "questions": "/api/bids/f73e6c4d-9f2d-4e66-8c57-7ae8d20a558e/questions", + "self": "/api/bids/f73e6c4d-9f2d-4e66-8c57-7ae8d20a558e" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 34 + } + ], + "tender": "Data Integration and Reporting", + "was_successful": false + }, + { + "_id": "d4e0f4d7-89a1-23b4-c5d6-e7f8a9b0c1d2", + "alias": "GlobeTech", + "bid_date": "2023-07-20", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "GlobeTech Innovations", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 24 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-08-07T16:45:10.987654", + "links": { + "questions": "/api/bids/d4e0f4d7-89a1-23b4-c5d6-e7f8a9b0c1d2/questions", + "self": "/api/bids/d4e0f4d7-89a1-23b4-c5d6-e7f8a9b0c1d2" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 26 + } + ], + "tender": "Data Solutions Architecture", + "was_successful": false + }, + { + "_id": "c3a2b1d5-e8f7-4c6d-a9b8-7f6e5d4c3b2a", + "alias": "DataCo", + "bid_date": "2023-07-25", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "DataCo Services", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 19 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-08-09T10:30:00.123456", + "links": { + "questions": "/api/bids/c3a2b1d5-e8f7-4c6d-a9b8-7f6e5d4c3b2a/questions", + "self": "/api/bids/c3a2b1d5-e8f7-4c6d-a9b8-7f6e5d4c3b2a" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 31 + } + ], + "tender": "Data Governance and Compliance", + "was_successful": false + }, + { + "_id": "d9e8f7c6-b5a4-1d3c-7e6f-9b8a0c2d4e5f", + "alias": "TechConnect", + "bid_date": "2023-07-30", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "TechConnect Solutions", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 21 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-08-11T08:15:30.987654", + "links": { + "questions": "/api/bids/d9e8f7c6-b5a4-1d3c-7e6f-9b8a0c2d4e5f/questions", + "self": "/api/bids/d9e8f7c6-b5a4-1d3c-7e6f-9b8a0c2d4e5f" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 33 + } + ], + "tender": "Data Infrastructure Modernization", + "was_successful": false + }, + { + "_id": "f2e4d6c8-a0b1-42c3-d4e5-f6a7b8c9d0e1", + "alias": "DataGenius", + "bid_date": "2023-08-05", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "DataGenius Labs", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 23 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-08-13T16:30:00.654321", + "links": { + "questions": "/api/bids/f2e4d6c8-a0b1-42c3-d4e5-f6a7b8c9d0e1/questions", + "self": "/api/bids/f2e4d6c8-a0b1-42c3-d4e5-f6a7b8c9d0e1" + }, + "status": "in_progress", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 35 + } + ], + "tender": "Data Science and Analytics", + "was_successful": false + }, + { + "_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6f", + "alias": "InnoTech", + "bid_date": "2023-07-15", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "Innovative Technologies Ltd.", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 10 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-08-05T15:30:40.987654", + "links": { + "questions": "/api/bids/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6f/questions", + "self": "/api/bids/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6f" + }, + "status": "completed", + "success": [ + { + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 30 + } + ], + "tender": "Innovative Solutions Development", + "was_successful": false + }, + { + "_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4e", + "alias": "TechBiz", + "bid_date": "2023-07-18", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "TechBiz Solutions", + "failed": { + "has_score": true, + "out_of": 36, + "phase": 2, + "score": 19 + }, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-08-07T12:30:15.987654", + "links": { + "questions": "/api/bids/9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4e/questions", + "self": "/api/bids/9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4e" + }, + "status": "deleted", + "success": [ { - "_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": "/api/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e/questions", - "self": "/api/bids/2529a0be-6e1c-4202-92c7-65c3742dfd4e" - }, - "status": "deleted", - "success": [ - { - "has_score": true, - "out_of": 36, - "phase": 1, - "score": 30 - } - ], - "tender": "Business Intelligence and Data Warehousing", - "was_successful": false + "has_score": true, + "out_of": 36, + "phase": 1, + "score": 27 } ], - "total_count": 3 - } \ No newline at end of file + "tender": "Technology Consulting Services", + "was_successful": false + } +] diff --git a/scripts/test_data/questions.json b/scripts/test_data/questions.json index 3ec461f..4332f14 100644 --- a/scripts/test_data/questions.json +++ b/scripts/test_data/questions.json @@ -169,6 +169,25 @@ "response": "This is a negative response.", "score": 1, "status": "deleted" + }, + { + "_id": "b3f1d6f7-5e8c-4b9a-9d18-6a0c1e2d3f4g", + "description": "Tenth question", + "feedback": { + "description": "Average feedback", + "url": "https://organisation.sharepoint.com/Docs/tenthfolder" + }, + "last_updated": "2023-08-04T14:30:40.123456", + "links": { + "bid": "", + "self": "/questions/b3f1d6f7-5e8c-4b9a-9d18-6a0c1e2d3f4g" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/tenthfolder", + "respondents": ["JaneSmith"], + "response": "This is the tenth response.", + "score": 2, + "status": "in_progress" } ] From ac2fcf4032fe438e116d45f1af9fb9b7b7afe974 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 8 Aug 2023 18:49:10 +0100 Subject: [PATCH 171/208] refactor: refactor and renamed validate_questions_sort --- api/controllers/question_controller.py | 10 ++++------ helpers/helpers.py | 9 ++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 4029476..4048701 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -18,7 +18,7 @@ require_jwt, require_admin_access, validate_pagination, - validate_sort, + validate_questions_sort, ) question = Blueprint("question", __name__) @@ -52,7 +52,7 @@ def get_questions(bid_id): try: bid_id = validate_id_path(bid_id) hostname = request.headers.get("host") - field, order = validate_sort(request.args.get("sort")) + field, order = validate_questions_sort(request.args.get("sort")) limit, offset = validate_pagination( request.args.get("limit"), request.args.get("offset") ) @@ -89,8 +89,8 @@ def get_questions(bid_id): return showValidationError(error), 400 except ValueError as error: return jsonify({"Error": str(error)}), 400 - except Exception as e: - return str(e), 500 + except Exception: + return showInternalServerError(), 500 @question.route("/bids//questions/", methods=["GET"]) @@ -146,10 +146,8 @@ def update_question(bid_id, question_id): "links.self": f"/api/bids/{bid_id}/questions/{question_id}", } ) - # Return 404 response if not found / returns None if not data: return showNotFoundError(), 404 - updated_question = validate_question_update(request.get_json(), data) db["questions"].replace_one({"_id": question_id}, updated_question) return updated_question, 200 diff --git a/helpers/helpers.py b/helpers/helpers.py index ef22697..25c6a87 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -186,14 +186,13 @@ def validate_param(value, default_value, max_value, param_name): return valid_limit, valid_offset -def validate_sort(sort_value): +def validate_questions_sort(sort_value): + field = "description" + order = 1 if sort_value: if sort_value[0] == "-": field = sort_value[1:] order = -1 else: field = sort_value - order = 1 - return field, order - else: - return "description", 1 + return field, order From e54bb376cb5bf28bfaef869351ad51079e931c57 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 9 Aug 2023 09:51:36 +0100 Subject: [PATCH 172/208] test: added tests for invalid pagination --- tests/conftest.py | 17 +- tests/test_get_questions.py | 300 +++++++++++++++++++++++++++++++++++- tests/test_helpers.py | 30 +++- 3 files changed, 333 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 51e8f86..dcb1170 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,8 @@ from app import app from dotenv import load_dotenv +load_dotenv() + @pytest.fixture(scope="session") def test_client(): @@ -19,7 +21,6 @@ def test_client(): @pytest.fixture(scope="session") def api_key(): - load_dotenv() api_key = os.getenv("API_KEY") return api_key @@ -27,7 +28,6 @@ def api_key(): @pytest.fixture(scope="session") def basic_jwt(): payload = {"username": "User McTestface", "admin": False} - load_dotenv() key = os.getenv("SECRET_KEY") token = jwt.encode(payload=payload, key=key) return token @@ -36,7 +36,18 @@ def basic_jwt(): @pytest.fixture(scope="session") def admin_jwt(): payload = {"username": "Admin McTestface", "admin": True} - load_dotenv() key = os.getenv("SECRET_KEY") token = jwt.encode(payload=payload, key=key) return token + + +@pytest.fixture(scope="session") +def max_offset(): + max_offset = os.getenv("MAX_OFFSET") + return max_offset + + +@pytest.fixture(scope="session") +def max_limit(): + max_limit = os.getenv("MAX_LIMIT") + return max_limit diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index 060ce92..029b9f4 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -1,6 +1,8 @@ """ This file contains tests for the GET /bids/{bid_id}/questions endpoint. """ +import os +from dotenv import load_dotenv from unittest.mock import patch @@ -33,7 +35,9 @@ def test_get_questions_success(mock_db, test_client, basic_jwt): mock_db[ "questions" - ].find.return_value.skip.return_value.limit.return_value = sample_data + ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( + sample_data + ) mock_db["questions"].count_documents.return_value = len(sample_data) @@ -80,7 +84,9 @@ def test_links_with_host(mock_db, test_client, basic_jwt): # Mock the database find method to return the filtered sample data mock_db[ "questions" - ].find.return_value.skip.return_value.limit.return_value = sample_data + ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( + sample_data + ) mock_db["questions"].count_documents.return_value = len(sample_data) @@ -178,7 +184,7 @@ def test_no_questions_found(mock_db, test_client, basic_jwt): # Case 6: Validation error @patch("api.controllers.question_controller.db") -def test_get_question_by_id_validation_error(mock_db, test_client, basic_jwt): +def test_get_questions_bid_id_validation_error(mock_db, test_client, basic_jwt): # Set up the sample question ID sample_bid_id = "Invalid bid Id" # Make a request to the endpoint to get the questions @@ -188,3 +194,291 @@ def test_get_question_by_id_validation_error(mock_db, test_client, basic_jwt): ) assert response.status_code == 400 assert response.get_json() == {"Error": "{'id': ['Invalid Id']}"} + + +# Case 7: Invalid offset - greater than maximum +@patch("api.controllers.question_controller.db") +def test_get_questions_max_offset(mock_db, test_client, basic_jwt, max_offset): + invalid_offset = int(max_offset) + 1 + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_data = [ + { + "_id": "2b18f477-627f-4d48-a008-ca0d9cea3791", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/api/bids/{sample_bid_id}", + "self": f"/api/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + }, + ] + + mock_db[ + "questions" + ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( + sample_data + ) + + mock_db["questions"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions?offset={invalid_offset}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Offset value must be a number between 0 and {max_offset}" + ) + + +# Case 8: Invalid offset - not a number +@patch("api.controllers.question_controller.db") +def test_get_questions_nan_offset(mock_db, test_client, basic_jwt, max_offset): + invalid_offset = "five" + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_data = [ + { + "_id": "2b18f477-627f-4d48-a008-ca0d9cea3791", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/api/bids/{sample_bid_id}", + "self": f"/api/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + }, + ] + + mock_db[ + "questions" + ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( + sample_data + ) + + mock_db["questions"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions?offset={invalid_offset}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Offset value must be a number between 0 and {max_offset}" + ) + + +# Case 9: Invalid offset - negative number +@patch("api.controllers.question_controller.db") +def test_get_questions_negative_offset(mock_db, test_client, basic_jwt, max_offset): + invalid_offset = -1 + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_data = [ + { + "_id": "2b18f477-627f-4d48-a008-ca0d9cea3791", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/api/bids/{sample_bid_id}", + "self": f"/api/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + }, + ] + + mock_db[ + "questions" + ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( + sample_data + ) + + mock_db["questions"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions?offset={invalid_offset}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Offset value must be a number between 0 and {max_offset}" + ) + + +# Case 10: Invalid limit - greater than maximum +@patch("api.controllers.question_controller.db") +def test_get_questions_max_limit(mock_db, test_client, basic_jwt, max_limit): + invalid_limit = int(max_limit) + 1 + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_data = [ + { + "_id": "2b18f477-627f-4d48-a008-ca0d9cea3791", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/api/bids/{sample_bid_id}", + "self": f"/api/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + }, + ] + + mock_db[ + "questions" + ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( + sample_data + ) + + mock_db["questions"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions?limit={invalid_limit}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Limit value must be a number between 0 and {max_limit}" + ) + + +# Case 11: Invalid limit - not a number +@patch("api.controllers.question_controller.db") +def test_get_questions_nan_limit(mock_db, test_client, basic_jwt, max_limit): + invalid_limit = "ten" + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_data = [ + { + "_id": "2b18f477-627f-4d48-a008-ca0d9cea3791", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/api/bids/{sample_bid_id}", + "self": f"/api/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + }, + ] + + mock_db[ + "questions" + ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( + sample_data + ) + + mock_db["questions"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions?limit={invalid_limit}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Limit value must be a number between 0 and {max_limit}" + ) + + +# Case 12: Invalid limit - negative number +@patch("api.controllers.question_controller.db") +def test_get_questions_negative_limit(mock_db, test_client, basic_jwt, max_limit): + invalid_limit = -1 + sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" + sample_data = [ + { + "_id": "2b18f477-627f-4d48-a008-ca0d9cea3791", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + "last_updated": "2023-08-01T23:11:59.336092", + "links": { + "bid": f"/api/bids/{sample_bid_id}", + "self": f"/api/bids/{sample_bid_id}/questions/2b18f477-627f-4d48-a008-ca0d9cea3791", + }, + "out_of": None, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": [], + "response": None, + "score": None, + "status": "in_progress", + }, + ] + + mock_db[ + "questions" + ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( + sample_data + ) + + mock_db["questions"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the questions + response = test_client.get( + f"api/bids/{sample_bid_id}/questions?limit={invalid_limit}", + headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Limit value must be a number between 0 and {max_limit}" + ) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 5a8ce56..0b8e64d 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,8 +1,14 @@ """ This file contains tests for the helper functions in helpers.py """ +import os import pytest -from helpers.helpers import prepend_host_to_links, validate_pagination +from dotenv import load_dotenv +from helpers.helpers import ( + prepend_host_to_links, + validate_pagination, + validate_questions_sort, +) # Case 1: Host is prepended to values in links object @@ -33,6 +39,11 @@ def test_prepend_host(): def test_validate_pagination(): + load_dotenv() + default_limit = os.getenv("DEFAULT_LIMIT") + max_limit = os.getenv("MAX_LIMIT") + default_offset = os.getenv("DEFAULT_OFFSET") + max_offset = os.getenv("MAX_OFFSET") valid_limit = 10 valid_offset = 20 nan_limit = "five" @@ -40,22 +51,25 @@ def test_validate_pagination(): negative_limit = -5 negative_offset = -10 - assert validate_pagination(valid_limit, valid_offset) == (10, 20) - assert validate_pagination(None, valid_offset) == (20, 20) - assert validate_pagination(valid_limit, None) == (10, 0) + assert validate_pagination(valid_limit, valid_offset) == (valid_limit, valid_offset) + assert validate_pagination(None, valid_offset) == (int(default_limit), valid_offset) + assert validate_pagination(valid_limit, None) == (valid_limit, int(default_offset)) with pytest.raises( - ValueError, match="Limit value must be a number between 0 and 1000" + ValueError, match=f"Limit value must be a number between 0 and {max_limit}" ): validate_pagination(nan_limit, valid_offset) with pytest.raises( - ValueError, match="Limit value must be a number between 0 and 1000" + ValueError, match=f"Limit value must be a number between 0 and {max_limit}" ): validate_pagination(negative_limit, valid_offset) with pytest.raises( - ValueError, match="Offset value must be a number between 0 and 2000" + ValueError, match=f"Offset value must be a number between 0 and {max_offset}" ): validate_pagination(valid_limit, nan_offset) with pytest.raises( - ValueError, match="Offset value must be a number between 0 and 2000" + ValueError, match=f"Offset value must be a number between 0 and {max_offset}" ): validate_pagination(valid_limit, negative_offset) + + +# def test_validate_questions_sort(): From 63e687139e46bb4347a708f3f6215c1c0e5be4c4 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 9 Aug 2023 09:55:52 +0100 Subject: [PATCH 173/208] test: added reused test variables to configtest file as fixtures --- tests/conftest.py | 12 ++++++++++++ tests/test_helpers.py | 7 +------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index dcb1170..94ed086 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,3 +51,15 @@ def max_offset(): def max_limit(): max_limit = os.getenv("MAX_LIMIT") return max_limit + + +@pytest.fixture(scope="session") +def default_offset(): + default_offset = os.getenv("DEFAULT_OFFSET") + return default_offset + + +@pytest.fixture(scope="session") +def default_limit(): + default_limit = os.getenv("DEFAULT_LIMIT") + return default_limit diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 0b8e64d..cefb578 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -38,12 +38,7 @@ def test_prepend_host(): } -def test_validate_pagination(): - load_dotenv() - default_limit = os.getenv("DEFAULT_LIMIT") - max_limit = os.getenv("MAX_LIMIT") - default_offset = os.getenv("DEFAULT_OFFSET") - max_offset = os.getenv("MAX_OFFSET") +def test_validate_pagination(default_limit, max_limit, default_offset, max_offset): valid_limit = 10 valid_offset = 20 nan_limit = "five" From 53c830b611b9a977c034cbd7939b7fbce42e666e Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 9 Aug 2023 10:07:02 +0100 Subject: [PATCH 174/208] docs: updated swagger for sort param --- static/swagger_config.yml | 12 ++++++++++++ tests/test_helpers.py | 2 -- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 582afe1..4979105 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -48,6 +48,12 @@ paths: summary: Returns a list of bids description: A JSON with item count and array of all bids parameters: + - name: sort + in: query + description: field for sorting list of results in ascending order ("-" before the field will denote descending order) + required: false + schema: + type: string - name: limit in: query description: number of results to return per page @@ -270,6 +276,12 @@ paths: schema: type: string format: uuid + - name: sort + in: query + description: field for sorting list of results in ascending order ("-" before the field will denote descending order) + required: false + schema: + type: string - name: limit in: query description: number of results to return per page diff --git a/tests/test_helpers.py b/tests/test_helpers.py index cefb578..5554d60 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,9 +1,7 @@ """ This file contains tests for the helper functions in helpers.py """ -import os import pytest -from dotenv import load_dotenv from helpers.helpers import ( prepend_host_to_links, validate_pagination, From 04f96c6e8513d8a531667f123b057ac8653673ef Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 9 Aug 2023 10:19:54 +0100 Subject: [PATCH 175/208] refactor: modified validate_sort helper to allow reuse --- api/controllers/question_controller.py | 4 ++-- helpers/helpers.py | 8 ++++++-- tests/test_helpers.py | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 4048701..d8e3dab 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -18,7 +18,7 @@ require_jwt, require_admin_access, validate_pagination, - validate_questions_sort, + validate_sort, ) question = Blueprint("question", __name__) @@ -52,7 +52,7 @@ def get_questions(bid_id): try: bid_id = validate_id_path(bid_id) hostname = request.headers.get("host") - field, order = validate_questions_sort(request.args.get("sort")) + field, order = validate_sort(request.args.get("sort"), "questions") limit, offset = validate_pagination( request.args.get("limit"), request.args.get("offset") ) diff --git a/helpers/helpers.py b/helpers/helpers.py index 25c6a87..0ca00db 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -186,8 +186,12 @@ def validate_param(value, default_value, max_value, param_name): return valid_limit, valid_offset -def validate_questions_sort(sort_value): - field = "description" +def validate_sort(sort_value, resource): + load_dotenv() + if resource == "bids": + field = os.getenv("DEFAULT_SORT_BIDS") + elif resource == "questions": + field = os.getenv("DEFAULT_SORT_QUESTIONS") order = 1 if sort_value: if sort_value[0] == "-": diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 5554d60..7bdfd9f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -5,7 +5,7 @@ from helpers.helpers import ( prepend_host_to_links, validate_pagination, - validate_questions_sort, + validate_sort, ) @@ -65,4 +65,4 @@ def test_validate_pagination(default_limit, max_limit, default_offset, max_offse validate_pagination(valid_limit, negative_offset) -# def test_validate_questions_sort(): +# def test_validate_sort(): From 472922378fef5f7927fbf5c28b2cc5b91e22195a Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Wed, 9 Aug 2023 10:52:24 +0100 Subject: [PATCH 176/208] test: bid test with pagination and sort --- api/controllers/bid_controller.py | 20 ++++----- request_examples/get_all_bids.http | 2 +- tests/test_get_bids.py | 68 ++++++++++++++++-------------- 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 74cd9d5..a5881ad 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -49,13 +49,13 @@ def get_bids(): data = list(db["bids"].find(query_filter, **query_options)) total_count = db["bids"].count_documents(query_filter) - explain_result = ( - db["bids"] - .find({"status": {"$ne": Status.DELETED.value}}) - .sort("alias", order) - .explain() - ) - print(explain_result["queryPlanner"]["winningPlan"]) + # explain_result = ( + # db["bids"] + # .find({"status": {"$ne": Status.DELETED.value}}) + # .sort("alias", order) + # .explain() + # ) + # print(explain_result["queryPlanner"]["winningPlan"]) # Return the response if not data: return showNotFoundError(), 404 @@ -103,7 +103,7 @@ def get_bid_by_id(bid_id): {"_id": bid_id, "status": {"$ne": Status.DELETED.value}} ) # Return 404 response if not found / returns None - if data is None: + if not data: return showNotFoundError(), 404 # Get hostname from request headers hostname = request.headers.get("host") @@ -128,7 +128,7 @@ def update_bid_by_id(bid_id): {"_id": bid_id, "status": Status.IN_PROGRESS.value} ) # Return 404 response if not found / returns None - if current_bid is None: + if not current_bid: return showNotFoundError(), 404 updated_bid = validate_bid_update(request.get_json(), current_bid) db["bids"].replace_one( @@ -160,7 +160,7 @@ def change_status_to_deleted(bid_id): } }, ) - if data is None: + if not data: return showNotFoundError(), 404 return data, 204 # Return 400 response if input validation fails diff --git a/request_examples/get_all_bids.http b/request_examples/get_all_bids.http index b419266..51e3ef0 100644 --- a/request_examples/get_all_bids.http +++ b/request_examples/get_all_bids.http @@ -1,2 +1,2 @@ -GET http://localhost:8080/api/bids?sort=alias&order=1 HTTP/1.1 +GET http://localhost:8080/api/bids HTTP/1.1 X-API-Key: THIS_IS_THE_API_KEY \ No newline at end of file diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 25ad5e2..d61b21f 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -1,41 +1,49 @@ from unittest.mock import patch +# Import the necessary modules +from unittest.mock import patch + + # Case 1: Successful get @patch("api.controllers.bid_controller.db") def test_get_bids_success(mock_db, test_client, api_key): - # Mock the find method - mock_db["bids"].find().skip().limit.return_value = [ + # Mock the find method of the db object + sample_data = [ { "_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", + "bids": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/bids", "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", }, "status": "in_progress", "tender": "Business Intelligence and Data Warehousing", } ] + mock_db["bids"].find.return_value = sample_data + # mock_db["bids"].find.return_value.skip.return_value.limit.return_value = sample_data + mock_db["bids"].count_documents.return_value = len(sample_data) - response_data = test_client.get( - "/api/bids", headers={"host": "localhost:8080", "X-API-Key": api_key} + response = test_client.get( + "/api/bids", # Provide correct query parameters + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) - assert response_data.status_code == 200 - # Assert response structure - assert "count" in response_data - assert "total_count" in response_data - assert "limit" in response_data - assert "offset" in response_data - assert "data" in response_data + # # Assert the response status code and content + assert response.status_code == 200 + response_data = response.get_json() + assert response_data["total_count"] == len(sample_data) + assert response_data["data"] == sample_data + assert response_data["limit"] == 5 + assert response_data["offset"] == 0 # Case 2: Links prepended with hostname @patch("api.controllers.bid_controller.db") def test_links_with_host(mock_db, test_client, api_key): - mock_db["bids"].find.return_value = [ + sample_data = [ { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", "bid_date": "2023-06-23", @@ -49,28 +57,26 @@ def test_links_with_host(mock_db, test_client, api_key): } ] + mock_db["bids"].find.return_value = sample_data + mock_db["bids"].count_documents.return_value = len(sample_data) + response = test_client.get( "/api/bids", headers={"host": "localhost:8080", "X-API-Key": api_key} ) assert response.status_code == 200 - assert response.get_json() == { - "total_count": 1, - "limit": 5, - "offset": 0, - "data": [ - { - "_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", - } - ], - } + response_data = response.get_json() + assert response_data["total_count"] == len(sample_data) + assert response_data["data"] == sample_data + assert response_data["limit"] == 5 + assert response_data["offset"] == 0 + assert ( + response_data["data"][0]["links"]["questions"] + == "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions" + ) + assert ( + response_data["data"][0]["links"]["self"] + == "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301" + ) # Case 3: Connection error From d73b11b2407c453774f783b5c7b22a925d46f9c2 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 9 Aug 2023 13:53:54 +0100 Subject: [PATCH 177/208] feat: added create and drop indexes commands to updated swagger with responses for resource lists --- api/controllers/bid_controller.py | 38 +++++------------ api/controllers/question_controller.py | 29 +++++-------- scripts/create_bids.py | 12 +++++- scripts/create_questions.py | 15 ++++--- scripts/delete_bids.py | 2 +- scripts/delete_questions.py | 2 +- static/swagger_config.yml | 18 ++++++++ tests/conftest.py | 8 ++-- tests/test_get_bids.py | 13 ++---- tests/test_get_questions.py | 59 +++++++------------------- tests/test_helpers.py | 2 + 11 files changed, 87 insertions(+), 111 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index a5881ad..23f6972 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -20,6 +20,8 @@ require_api_key, require_jwt, require_admin_access, + validate_sort, + validate_pagination, ) bid = Blueprint("bid", __name__) @@ -29,38 +31,20 @@ @require_api_key def get_bids(): try: - # Validate and process the limit parameter - limit = int(request.args.get("limit", 5)) - if limit < 1: - return jsonify({"error": "Limit must be a positive number"}), 400 - - offset = int(request.args.get("offset", 0)) - if offset < 0 or offset >= 1000: - return jsonify({"error": "Offset must be between 0 and 999"}), 400 - - sort = request.args.get("sort", "last_updated") - order = int(request.args.get("order", 1)) # Default to ascending (1) + hostname = request.headers.get("host") + field, order = validate_sort(request.args.get("sort"), "questions") + limit, offset = validate_pagination( + request.args.get("limit"), request.args.get("offset") + ) # Prepare query filter and options query_filter = {"status": {"$ne": Status.DELETED.value}} - query_options = {"sort": [(sort, order)], "skip": offset, "limit": limit} + query_options = {"sort": [(field, order)], "skip": offset, "limit": limit} # Fetch data and count documents data = list(db["bids"].find(query_filter, **query_options)) total_count = db["bids"].count_documents(query_filter) - # explain_result = ( - # db["bids"] - # .find({"status": {"$ne": Status.DELETED.value}}) - # .sort("alias", order) - # .explain() - # ) - # print(explain_result["queryPlanner"]["winningPlan"]) - # Return the response - if not data: - return showNotFoundError(), 404 - - hostname = request.headers.get("host") for resource in data: prepend_host_to_links(resource, hostname) @@ -69,10 +53,10 @@ def get_bids(): "total_count": total_count, "limit": limit, "offset": offset, - "data": data, + "items": data, }, 200 - except ValueError: # Handle non-integer values - return jsonify({"error": "Page and limit must be valid integer values"}), 400 + except ValueError as error: + return jsonify({"Error": str(error)}), 400 except Exception: return showInternalServerError(), 500 diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index d8e3dab..5caad89 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -56,24 +56,17 @@ def get_questions(bid_id): limit, offset = validate_pagination( request.args.get("limit"), request.args.get("offset") ) - data = list( - db["questions"] - .find( - { - "status": {"$ne": Status.DELETED.value}, - "links.bid": f"/api/bids/{bid_id}", - } - ) - .sort(field, order) - .skip(offset) - .limit(limit) - ) - total_count = db["questions"].count_documents( - { - "status": {"$ne": Status.DELETED.value}, - "links.bid": f"/api/bids/{bid_id}", - } - ) + # Prepare query filter and options + query_filter = { + "status": {"$ne": Status.DELETED.value}, + "links.bid": f"/api/bids/{bid_id}", + } + query_options = {"sort": [(field, order)], "skip": offset, "limit": limit} + + # Fetch data and count documents + data = list(db["questions"].find(query_filter, **query_options)) + total_count = db["questions"].count_documents(query_filter) + if not data: return showNotFoundError(), 404 for question in data: diff --git a/scripts/create_bids.py b/scripts/create_bids.py index ce7cb64..e784364 100644 --- a/scripts/create_bids.py +++ b/scripts/create_bids.py @@ -7,7 +7,7 @@ import os import json import sys -from pymongo import MongoClient +from pymongo import MongoClient, operations from pymongo.errors import ConnectionFailure from dotenv import load_dotenv @@ -40,6 +40,16 @@ def populate_bids(): with open(file_path, encoding="utf-8") as bids_file: bids_data = json.load(bids_file) + # Define the index models + index_models = [ + operations.IndexModel([("client", 1)]), + operations.IndexModel([("tender", 1)]), + operations.IndexModel([("bid_date", 1)]), + operations.IndexModel([("last_updated", -1)]), + ] + + # Create the indexes + collection.create_indexes(index_models) # Insert bids into the database for bid in bids_data: # Check if the bid already exists in the database diff --git a/scripts/create_questions.py b/scripts/create_questions.py index f059714..63089a6 100644 --- a/scripts/create_questions.py +++ b/scripts/create_questions.py @@ -9,8 +9,7 @@ import os import sys import uuid -from itertools import zip_longest -from pymongo import MongoClient +from pymongo import MongoClient, operations from pymongo.errors import ConnectionFailure from dotenv import load_dotenv @@ -50,8 +49,16 @@ def populate_questions(): with open(questions_path, encoding="utf-8") as questions_file: questions_data = json.load(questions_file) - # Update questions data with existing bid ids from bids.json + # Define index models + index_models = [ + operations.IndexModel([("description", 1)]), + operations.IndexModel([("last_updated", -1)]), + ] + + # Create index on default sort field + collection.create_indexes(index_models) + # Update questions data with existing bid ids from bids.json updated_questions = [] for bid in bids_data: @@ -77,8 +84,6 @@ def populate_questions(): collection.insert_one(question) print(f"Inserted question with _id: {question['_id']}") - collection.create_index("description") - except ConnectionFailure: print("Error: Failed to connect to database") sys.exit(1) diff --git a/scripts/delete_bids.py b/scripts/delete_bids.py index aa06d86..2f7f00d 100644 --- a/scripts/delete_bids.py +++ b/scripts/delete_bids.py @@ -33,7 +33,7 @@ def delete_bids(): else: delete_result = collection.delete_many({}) print(f"Deleted {delete_result.deleted_count} bids from the collection.") - + collection.drop_indexes() except ConnectionFailure: print("Error: Failed to connect to database") sys.exit(1) diff --git a/scripts/delete_questions.py b/scripts/delete_questions.py index aee7b2b..532ee9e 100644 --- a/scripts/delete_questions.py +++ b/scripts/delete_questions.py @@ -35,7 +35,7 @@ def delete_bids(): print( f"Deleted {delete_result.deleted_count} questions from the collection." ) - + collection.drop_indexes() except ConnectionFailure: print("Error: Failed to connect to database") sys.exit(1) diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 4979105..603c7b3 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -81,6 +81,15 @@ paths: total_count: type: integer example: 1 + count: + type: integer + example: 1 + offset: + type: integer + example: 0 + limit: + type: integer + example: 20 items: type: array items: @@ -309,6 +318,15 @@ paths: total_count: type: integer example: 1 + count: + type: integer + example: 1 + offset: + type: integer + example: 0 + limit: + type: integer + example: 20 items: type: array items: diff --git a/tests/conftest.py b/tests/conftest.py index 94ed086..b5d890a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,22 +44,22 @@ def admin_jwt(): @pytest.fixture(scope="session") def max_offset(): max_offset = os.getenv("MAX_OFFSET") - return max_offset + return int(max_offset) @pytest.fixture(scope="session") def max_limit(): max_limit = os.getenv("MAX_LIMIT") - return max_limit + return int(max_limit) @pytest.fixture(scope="session") def default_offset(): default_offset = os.getenv("DEFAULT_OFFSET") - return default_offset + return int(default_offset) @pytest.fixture(scope="session") def default_limit(): default_limit = os.getenv("DEFAULT_LIMIT") - return default_limit + return int(default_limit) diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index d61b21f..116f6c3 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -1,13 +1,9 @@ from unittest.mock import patch -# Import the necessary modules -from unittest.mock import patch - - # Case 1: Successful get @patch("api.controllers.bid_controller.db") -def test_get_bids_success(mock_db, test_client, api_key): +def test_get_bids_success(mock_db, test_client, api_key, default_limit, default_offset): # Mock the find method of the db object sample_data = [ { @@ -23,7 +19,6 @@ def test_get_bids_success(mock_db, test_client, api_key): } ] mock_db["bids"].find.return_value = sample_data - # mock_db["bids"].find.return_value.skip.return_value.limit.return_value = sample_data mock_db["bids"].count_documents.return_value = len(sample_data) response = test_client.get( @@ -36,8 +31,8 @@ def test_get_bids_success(mock_db, test_client, api_key): response_data = response.get_json() assert response_data["total_count"] == len(sample_data) assert response_data["data"] == sample_data - assert response_data["limit"] == 5 - assert response_data["offset"] == 0 + assert response_data["limit"] == default_limit + assert response_data["offset"] == default_offset # Case 2: Links prepended with hostname @@ -67,8 +62,6 @@ def test_links_with_host(mock_db, test_client, api_key): response_data = response.get_json() assert response_data["total_count"] == len(sample_data) assert response_data["data"] == sample_data - assert response_data["limit"] == 5 - assert response_data["offset"] == 0 assert ( response_data["data"][0]["links"]["questions"] == "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions" diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index 029b9f4..af54e94 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -1,14 +1,15 @@ """ This file contains tests for the GET /bids/{bid_id}/questions endpoint. """ -import os -from dotenv import load_dotenv + from unittest.mock import patch # Case 1: Successful get @patch("api.controllers.question_controller.db") -def test_get_questions_success(mock_db, test_client, basic_jwt): +def test_get_questions_success( + mock_db, test_client, basic_jwt, default_limit, default_offset +): # Set up the sample data and expected result sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_data = [ @@ -33,11 +34,7 @@ def test_get_questions_success(mock_db, test_client, basic_jwt): }, ] - mock_db[ - "questions" - ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( - sample_data - ) + mock_db["questions"].find.return_value = sample_data mock_db["questions"].count_documents.return_value = len(sample_data) @@ -52,6 +49,8 @@ def test_get_questions_success(mock_db, test_client, basic_jwt): response_data = response.get_json() assert response_data["total_count"] == len(sample_data) assert response_data["items"] == sample_data + assert response_data["limit"] == default_limit + assert response_data["offset"] == default_offset # Case 2: Links prepended with hostname @@ -82,11 +81,7 @@ def test_links_with_host(mock_db, test_client, basic_jwt): ] # Mock the database find method to return the filtered sample data - mock_db[ - "questions" - ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( - sample_data - ) + mock_db["questions"].find.return_value = sample_data mock_db["questions"].count_documents.return_value = len(sample_data) @@ -168,7 +163,7 @@ def test_no_questions_found(mock_db, test_client, basic_jwt): sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" # Mock the database find method to return an empty list - mock_db["questions"].find.return_value.skip.return_value.limit.return_value = [] + mock_db["questions"].find.return_value = [] # Make a request to the endpoint to get the questions response = test_client.get( @@ -223,11 +218,7 @@ def test_get_questions_max_offset(mock_db, test_client, basic_jwt, max_offset): }, ] - mock_db[ - "questions" - ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( - sample_data - ) + mock_db["questions"].find.return_value = sample_data mock_db["questions"].count_documents.return_value = len(sample_data) @@ -271,11 +262,7 @@ def test_get_questions_nan_offset(mock_db, test_client, basic_jwt, max_offset): }, ] - mock_db[ - "questions" - ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( - sample_data - ) + mock_db["questions"].find.return_value = sample_data mock_db["questions"].count_documents.return_value = len(sample_data) @@ -319,11 +306,7 @@ def test_get_questions_negative_offset(mock_db, test_client, basic_jwt, max_offs }, ] - mock_db[ - "questions" - ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( - sample_data - ) + mock_db["questions"].find.return_value = sample_data mock_db["questions"].count_documents.return_value = len(sample_data) @@ -367,11 +350,7 @@ def test_get_questions_max_limit(mock_db, test_client, basic_jwt, max_limit): }, ] - mock_db[ - "questions" - ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( - sample_data - ) + mock_db["questions"].find.return_value = sample_data mock_db["questions"].count_documents.return_value = len(sample_data) @@ -415,11 +394,7 @@ def test_get_questions_nan_limit(mock_db, test_client, basic_jwt, max_limit): }, ] - mock_db[ - "questions" - ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( - sample_data - ) + mock_db["questions"].find.return_value = sample_data mock_db["questions"].count_documents.return_value = len(sample_data) @@ -463,11 +438,7 @@ def test_get_questions_negative_limit(mock_db, test_client, basic_jwt, max_limit }, ] - mock_db[ - "questions" - ].find.return_value.sort.return_value.skip.return_value.limit.return_value = ( - sample_data - ) + mock_db["questions"].find.return_value = sample_data mock_db["questions"].count_documents.return_value = len(sample_data) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 7bdfd9f..832a77c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -36,6 +36,7 @@ def test_prepend_host(): } +# Case 2: pagination values are validated correctly def test_validate_pagination(default_limit, max_limit, default_offset, max_offset): valid_limit = 10 valid_offset = 20 @@ -65,4 +66,5 @@ def test_validate_pagination(default_limit, max_limit, default_offset, max_offse validate_pagination(valid_limit, negative_offset) +# Case 3: sort value is validated correctly # def test_validate_sort(): From b26174afe21864c0e3313fb377217242c232aa5d Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 9 Aug 2023 15:14:08 +0100 Subject: [PATCH 178/208] test: added endpoint tests for get --- README.md | 4 +- api/controllers/bid_controller.py | 2 +- api/controllers/question_controller.py | 5 +- helpers/helpers.py | 26 ++- request_examples/get_all_question.http | 2 +- scripts/README.md | 8 +- tests/conftest.py | 12 ++ tests/test_get_bids.py | 222 ++++++++++++++++++++++++- tests/test_get_question_by_id.py | 20 +-- tests/test_get_questions.py | 44 ++--- tests/test_helpers.py | 16 +- 11 files changed, 307 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 8c37208..e4d7233 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,9 @@ To do this, create a `.env` file in your root folder, with the following key/val DEFAULT_OFFSET=0 DEFAULT_LIMIT=20 MAX_OFFSET=2000 - MAX_LIMIT=1000 + MAX_LIMIT=1000 + DEFAULT_SORT_BIDS=bid_date + DEFAULT_SORT_QUESTIONS=description -------------- diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 23f6972..5bd050e 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -32,7 +32,7 @@ def get_bids(): try: hostname = request.headers.get("host") - field, order = validate_sort(request.args.get("sort"), "questions") + field, order = validate_sort(request.args.get("sort"), "bids") limit, offset = validate_pagination( request.args.get("limit"), request.args.get("offset") ) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 5caad89..10d12db 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -15,6 +15,7 @@ validate_id_path, validate_question_update, prepend_host_to_links, + require_api_key, require_jwt, require_admin_access, validate_pagination, @@ -47,7 +48,7 @@ def post_question(bid_id): @question.route("/bids//questions", methods=["GET"]) -@require_jwt +@require_api_key def get_questions(bid_id): try: bid_id = validate_id_path(bid_id) @@ -87,7 +88,7 @@ def get_questions(bid_id): @question.route("/bids//questions/", methods=["GET"]) -@require_jwt +@require_api_key def get_question(bid_id, question_id): try: bid_id = validate_id_path(bid_id) diff --git a/helpers/helpers.py b/helpers/helpers.py index 0ca00db..6f7090e 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -190,13 +190,27 @@ def validate_sort(sort_value, resource): load_dotenv() if resource == "bids": field = os.getenv("DEFAULT_SORT_BIDS") + valid_fields = [ + "client", + "tender", + "bid_date", + "alias", + "status", + "last_updated", + "was_successful", + ] elif resource == "questions": field = os.getenv("DEFAULT_SORT_QUESTIONS") + valid_fields = ["description", "score", "respondents", "status", "last_updated"] order = 1 - if sort_value: - if sort_value[0] == "-": - field = sort_value[1:] - order = -1 - else: - field = sort_value + try: + if sort_value: + if sort_value[0] == "-": + field = sort_value[1:] + order = -1 + else: + field = sort_value + assert field in valid_fields + except AssertionError: + raise ValueError("Invalid sort criteria") return field, order diff --git a/request_examples/get_all_question.http b/request_examples/get_all_question.http index a04fc34..f0a47cd 100644 --- a/request_examples/get_all_question.http +++ b/request_examples/get_all_question.http @@ -1,2 +1,2 @@ GET http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU3MDIyNn0.93o-R3VGeNHAbfthiJ4odsA8n9eBu7ZKwU2KP06EE6E \ No newline at end of file +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlBpcmEiLCJhZG1pbiI6dHJ1ZSwiZXhwIjoxNjkyMTkxNjM1fQ.nyWHYtF4PCz7FLThu3gjHF7D5_JirwuOUWggDP73TBs \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md index 327dfe3..6e9b601 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -3,10 +3,10 @@ ## Database Cleanup Script ### Script Description -- The delete_bids.py script is used to delete all bids from the MongoDB collection. -- The delete_questions.py script is used to delete all questions from the MongoDB collection. -- The create_bids.py script is used to populate the MongoDB database with sample bids data from the bids.json file. -- The create_questions.py script is used to populate the MongoDB database with sample questions data from the questions.json file, using existing bid Ids from the bids.json file. +- The delete_bids.py script is used to delete all bids and drop all indexes from the MongoDB collection. +- The delete_questions.py script is used to delete all questions and drop all indexes from the MongoDB collection. +- The create_bids.py script is used to populate the MongoDB database with sample bids data from the bids.json file and create indexes. +- The create_questions.py script is used to populate the MongoDB database with sample questions data from the questions.json file, using existing bid Ids from the bids.json file and create indexes. ### Usage diff --git a/tests/conftest.py b/tests/conftest.py index b5d890a..17906da 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,3 +63,15 @@ def default_offset(): def default_limit(): default_limit = os.getenv("DEFAULT_LIMIT") return int(default_limit) + + +@pytest.fixture(scope="session") +def default_sort_bids(): + default_sort_bids = os.getenv("DEFAULT_SORT_BIDS") + return default_sort_bids + + +@pytest.fixture(scope="session") +def default_sort_questions(): + default_sort_questions = os.getenv("DEFAULT_SORT_QUESTIONS") + return default_sort_questions diff --git a/tests/test_get_bids.py b/tests/test_get_bids.py index 116f6c3..791d31a 100644 --- a/tests/test_get_bids.py +++ b/tests/test_get_bids.py @@ -30,7 +30,7 @@ def test_get_bids_success(mock_db, test_client, api_key, default_limit, default_ assert response.status_code == 200 response_data = response.get_json() assert response_data["total_count"] == len(sample_data) - assert response_data["data"] == sample_data + assert response_data["items"] == sample_data assert response_data["limit"] == default_limit assert response_data["offset"] == default_offset @@ -44,7 +44,7 @@ def test_links_with_host(mock_db, test_client, api_key): "bid_date": "2023-06-23", "client": "Office for National Statistics", "links": { - "questions": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions", + "bids": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/bids", "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", }, "status": "in_progress", @@ -61,13 +61,13 @@ def test_links_with_host(mock_db, test_client, api_key): assert response.status_code == 200 response_data = response.get_json() assert response_data["total_count"] == len(sample_data) - assert response_data["data"] == sample_data + assert response_data["items"] == sample_data assert ( - response_data["data"][0]["links"]["questions"] - == "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/questions" + response_data["items"][0]["links"]["bids"] + == "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/bids" ) assert ( - response_data["data"][0]["links"]["self"] + response_data["items"][0]["links"]["self"] == "http://localhost:8080/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301" ) @@ -91,3 +91,213 @@ def test_get_bids_unauthorized(mock_db, test_client): ) assert response.status_code == 401 assert response.get_json()["Error"] == "Unauthorized" + + +# Case 5: Invalid offset - greater than maximum +@patch("api.controllers.bid_controller.db") +def test_get_bids_max_offset(mock_db, test_client, api_key, max_offset): + invalid_offset = int(max_offset) + 1 + sample_data = [ + { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "bid_date": "2023-06-23", + "client": "Office for National Statistics", + "links": { + "bids": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/bids", + "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + } + ] + + mock_db["bids"].find.return_value = sample_data + + mock_db["bids"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the bids + response = test_client.get( + f"api/bids?offset={invalid_offset}", + headers={"host": "localhost:8080", "X-API-Key": api_key}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Offset value must be a number between 0 and {max_offset}" + ) + + +# Case 6: Invalid offset - not a number +@patch("api.controllers.bid_controller.db") +def test_get_bids_nan_offset(mock_db, test_client, api_key, max_offset): + invalid_offset = "five" + sample_data = [ + { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "bid_date": "2023-06-23", + "client": "Office for National Statistics", + "links": { + "bids": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/bids", + "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + } + ] + + mock_db["bids"].find.return_value = sample_data + + mock_db["bids"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the bids + response = test_client.get( + f"api/bids?offset={invalid_offset}", + headers={"host": "localhost:8080", "X-API-Key": api_key}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Offset value must be a number between 0 and {max_offset}" + ) + + +# Case 7: Invalid offset - negative number +@patch("api.controllers.bid_controller.db") +def test_get_bids_negative_offset(mock_db, test_client, api_key, max_offset): + invalid_offset = -1 + sample_data = [ + { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "bid_date": "2023-06-23", + "client": "Office for National Statistics", + "links": { + "bids": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/bids", + "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + } + ] + + mock_db["bids"].find.return_value = sample_data + + mock_db["bids"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the bids + response = test_client.get( + f"api/bids?offset={invalid_offset}", + headers={"host": "localhost:8080", "X-API-Key": api_key}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Offset value must be a number between 0 and {max_offset}" + ) + + +# Case 8: Invalid limit - greater than maximum +@patch("api.controllers.bid_controller.db") +def test_get_bids_max_limit(mock_db, test_client, api_key, max_limit): + invalid_limit = int(max_limit) + 1 + sample_data = [ + { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "bid_date": "2023-06-23", + "client": "Office for National Statistics", + "links": { + "bids": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/bids", + "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + } + ] + + mock_db["bids"].find.return_value = sample_data + + mock_db["bids"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the bids + response = test_client.get( + f"api/bids?limit={invalid_limit}", + headers={"host": "localhost:8080", "X-API-Key": api_key}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Limit value must be a number between 0 and {max_limit}" + ) + + +# Case 9: Invalid limit - not a number +@patch("api.controllers.bid_controller.db") +def test_get_bids_nan_limit(mock_db, test_client, api_key, max_limit): + invalid_limit = "five" + sample_data = [ + { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "bid_date": "2023-06-23", + "client": "Office for National Statistics", + "links": { + "bids": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/bids", + "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + } + ] + + mock_db["bids"].find.return_value = sample_data + + mock_db["bids"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the bids + response = test_client.get( + f"api/bids?limit={invalid_limit}", + headers={"host": "localhost:8080", "X-API-Key": api_key}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Limit value must be a number between 0 and {max_limit}" + ) + + +# Case 10: Invalid limit - negative number +@patch("api.controllers.bid_controller.db") +def test_get_bids_negative_limit(mock_db, test_client, api_key, max_limit): + invalid_limit = -1 + sample_data = [ + { + "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", + "bid_date": "2023-06-23", + "client": "Office for National Statistics", + "links": { + "bids": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301/bids", + "self": "/bids/faaf8ef5-5db4-459d-8d24-bc39492e1301", + }, + "status": "in_progress", + "tender": "Business Intelligence and Data Warehousing", + } + ] + + mock_db["bids"].find.return_value = sample_data + + mock_db["bids"].count_documents.return_value = len(sample_data) + + # Make a request to the endpoint to get the bids + response = test_client.get( + f"api/bids?limit={invalid_limit}", + headers={"host": "localhost:8080", "X-API-Key": api_key}, + ) + + assert response.status_code == 400 + assert ( + response.get_json()["Error"] + == f"Limit value must be a number between 0 and {max_limit}" + ) diff --git a/tests/test_get_question_by_id.py b/tests/test_get_question_by_id.py index 5221321..8bd3474 100644 --- a/tests/test_get_question_by_id.py +++ b/tests/test_get_question_by_id.py @@ -6,7 +6,7 @@ # Case 1: Successful get @patch("api.controllers.question_controller.db") -def test_get_single_question_success(mock_db, test_client, basic_jwt): +def test_get_single_question_success(mock_db, test_client, api_key): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" @@ -38,7 +38,7 @@ def test_get_single_question_success(mock_db, test_client, basic_jwt): # Make a request to the endpoint to get the single question response = test_client.get( f"api/bids/{sample_bid_id}/questions/{sample_question_id}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) # Assert the response status code and content @@ -49,7 +49,7 @@ def test_get_single_question_success(mock_db, test_client, basic_jwt): # Case 2: Links prepended with hostname @patch("api.controllers.question_controller.db") -def test_single_question_links_with_host(mock_db, test_client, basic_jwt): +def test_single_question_links_with_host(mock_db, test_client, api_key): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" @@ -81,7 +81,7 @@ def test_single_question_links_with_host(mock_db, test_client, basic_jwt): # Make a request to the endpoint to get the single question with hostname in the headers response = test_client.get( f"api/bids/{sample_bid_id}/questions/{sample_question_id}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) # Assert the response status code and content @@ -98,7 +98,7 @@ def test_single_question_links_with_host(mock_db, test_client, basic_jwt): # Case 3: Connection error @patch("api.controllers.question_controller.db") -def test_get_single_question_connection_error(mock_db, test_client, basic_jwt): +def test_get_single_question_connection_error(mock_db, test_client, api_key): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" @@ -109,7 +109,7 @@ def test_get_single_question_connection_error(mock_db, test_client, basic_jwt): # Make a request to the endpoint to get the single question response = test_client.get( f"api/bids/{sample_bid_id}/questions/{sample_question_id}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) # Assert the response status code and content @@ -153,7 +153,7 @@ def test_get_single_question_unauthorized(mock_db, test_client): # Case 5: No question found for the given ID @patch("api.controllers.question_controller.db") -def test_no_question_found_by_id(mock_db, test_client, basic_jwt): +def test_no_question_found_by_id(mock_db, test_client, api_key): # Set up the sample question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" @@ -164,7 +164,7 @@ def test_no_question_found_by_id(mock_db, test_client, basic_jwt): # Make a request to the endpoint to get the question by ID response = test_client.get( f"api/bids/{sample_bid_id}/questions/{sample_question_id}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) # Assert the response status code and content @@ -175,13 +175,13 @@ def test_no_question_found_by_id(mock_db, test_client, basic_jwt): # Case 6: Validation error @patch("api.controllers.question_controller.db") -def test_get_question_by_id_validation_error(mock_db, test_client, basic_jwt): +def test_get_question_by_id_validation_error(mock_db, test_client, api_key): # Set up the sample question ID sample_bid_id = "Invalid bid Id" sample_question_id = "2b18f477-627f-4d48-a008-ca0d9cea3791" response = test_client.get( f"api/bids/{sample_bid_id}/questions/{sample_question_id}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) assert response.status_code == 400 assert response.get_json() == {"Error": "{'id': ['Invalid Id']}"} diff --git a/tests/test_get_questions.py b/tests/test_get_questions.py index af54e94..19ffdca 100644 --- a/tests/test_get_questions.py +++ b/tests/test_get_questions.py @@ -8,7 +8,7 @@ # Case 1: Successful get @patch("api.controllers.question_controller.db") def test_get_questions_success( - mock_db, test_client, basic_jwt, default_limit, default_offset + mock_db, test_client, api_key, default_limit, default_offset ): # Set up the sample data and expected result sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -41,7 +41,7 @@ def test_get_questions_success( # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) # Assert the response status code and content @@ -55,7 +55,7 @@ def test_get_questions_success( # Case 2: Links prepended with hostname @patch("api.controllers.question_controller.db") -def test_links_with_host(mock_db, test_client, basic_jwt): +def test_links_with_host(mock_db, test_client, api_key): # Set up the sample data and expected result sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_data = [ @@ -88,7 +88,7 @@ def test_links_with_host(mock_db, test_client, basic_jwt): # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) # Assert the response status code and content @@ -106,7 +106,7 @@ def test_links_with_host(mock_db, test_client, basic_jwt): # Case 3: Connection error @patch("api.controllers.question_controller.db") -def test_get_questions_connection_error(mock_db, test_client, basic_jwt): +def test_get_questions_connection_error(mock_db, test_client, api_key): # Set up the sample bid ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -116,7 +116,7 @@ def test_get_questions_connection_error(mock_db, test_client, basic_jwt): # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) # Assert the response status code and content @@ -158,7 +158,7 @@ def test_get_questions_unauthorized(mock_db, test_client): # Case 5: No questions found @patch("api.controllers.question_controller.db") -def test_no_questions_found(mock_db, test_client, basic_jwt): +def test_no_questions_found(mock_db, test_client, api_key): # Set up the sample bid ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -168,7 +168,7 @@ def test_no_questions_found(mock_db, test_client, basic_jwt): # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) # Assert the response status code and content @@ -179,13 +179,13 @@ def test_no_questions_found(mock_db, test_client, basic_jwt): # Case 6: Validation error @patch("api.controllers.question_controller.db") -def test_get_questions_bid_id_validation_error(mock_db, test_client, basic_jwt): +def test_get_questions_bid_id_validation_error(mock_db, test_client, api_key): # Set up the sample question ID sample_bid_id = "Invalid bid Id" # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) assert response.status_code == 400 assert response.get_json() == {"Error": "{'id': ['Invalid Id']}"} @@ -193,7 +193,7 @@ def test_get_questions_bid_id_validation_error(mock_db, test_client, basic_jwt): # Case 7: Invalid offset - greater than maximum @patch("api.controllers.question_controller.db") -def test_get_questions_max_offset(mock_db, test_client, basic_jwt, max_offset): +def test_get_questions_max_offset(mock_db, test_client, api_key, max_offset): invalid_offset = int(max_offset) + 1 sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_data = [ @@ -225,7 +225,7 @@ def test_get_questions_max_offset(mock_db, test_client, basic_jwt, max_offset): # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions?offset={invalid_offset}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) assert response.status_code == 400 @@ -237,7 +237,7 @@ def test_get_questions_max_offset(mock_db, test_client, basic_jwt, max_offset): # Case 8: Invalid offset - not a number @patch("api.controllers.question_controller.db") -def test_get_questions_nan_offset(mock_db, test_client, basic_jwt, max_offset): +def test_get_questions_nan_offset(mock_db, test_client, api_key, max_offset): invalid_offset = "five" sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_data = [ @@ -269,7 +269,7 @@ def test_get_questions_nan_offset(mock_db, test_client, basic_jwt, max_offset): # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions?offset={invalid_offset}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) assert response.status_code == 400 @@ -281,7 +281,7 @@ def test_get_questions_nan_offset(mock_db, test_client, basic_jwt, max_offset): # Case 9: Invalid offset - negative number @patch("api.controllers.question_controller.db") -def test_get_questions_negative_offset(mock_db, test_client, basic_jwt, max_offset): +def test_get_questions_negative_offset(mock_db, test_client, api_key, max_offset): invalid_offset = -1 sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_data = [ @@ -313,7 +313,7 @@ def test_get_questions_negative_offset(mock_db, test_client, basic_jwt, max_offs # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions?offset={invalid_offset}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) assert response.status_code == 400 @@ -325,7 +325,7 @@ def test_get_questions_negative_offset(mock_db, test_client, basic_jwt, max_offs # Case 10: Invalid limit - greater than maximum @patch("api.controllers.question_controller.db") -def test_get_questions_max_limit(mock_db, test_client, basic_jwt, max_limit): +def test_get_questions_max_limit(mock_db, test_client, api_key, max_limit): invalid_limit = int(max_limit) + 1 sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_data = [ @@ -357,7 +357,7 @@ def test_get_questions_max_limit(mock_db, test_client, basic_jwt, max_limit): # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions?limit={invalid_limit}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) assert response.status_code == 400 @@ -369,7 +369,7 @@ def test_get_questions_max_limit(mock_db, test_client, basic_jwt, max_limit): # Case 11: Invalid limit - not a number @patch("api.controllers.question_controller.db") -def test_get_questions_nan_limit(mock_db, test_client, basic_jwt, max_limit): +def test_get_questions_nan_limit(mock_db, test_client, api_key, max_limit): invalid_limit = "ten" sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_data = [ @@ -401,7 +401,7 @@ def test_get_questions_nan_limit(mock_db, test_client, basic_jwt, max_limit): # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions?limit={invalid_limit}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) assert response.status_code == 400 @@ -413,7 +413,7 @@ def test_get_questions_nan_limit(mock_db, test_client, basic_jwt, max_limit): # Case 12: Invalid limit - negative number @patch("api.controllers.question_controller.db") -def test_get_questions_negative_limit(mock_db, test_client, basic_jwt, max_limit): +def test_get_questions_negative_limit(mock_db, test_client, api_key, max_limit): invalid_limit = -1 sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" sample_data = [ @@ -445,7 +445,7 @@ def test_get_questions_negative_limit(mock_db, test_client, basic_jwt, max_limit # Make a request to the endpoint to get the questions response = test_client.get( f"api/bids/{sample_bid_id}/questions?limit={invalid_limit}", - headers={"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"}, + headers={"host": "localhost:8080", "X-API-Key": api_key}, ) assert response.status_code == 400 diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 832a77c..14ed900 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -67,4 +67,18 @@ def test_validate_pagination(default_limit, max_limit, default_offset, max_offse # Case 3: sort value is validated correctly -# def test_validate_sort(): +def test_validate_sort(default_sort_bids, default_sort_questions): + valid_field_asc = "last_updated" + valid_field_desc = "-last_updated" + invalid_field = "invalid" + + assert validate_sort(valid_field_asc, "bids") == (valid_field_asc, 1) + assert validate_sort(valid_field_asc, "questions") == (valid_field_asc, 1) + assert validate_sort(valid_field_desc, "bids") == (valid_field_desc[1:], -1) + assert validate_sort(valid_field_desc, "questions") == (valid_field_desc[1:], -1) + assert validate_sort(None, "bids") == (default_sort_bids, 1) + assert validate_sort(None, "questions") == (default_sort_questions, 1) + with pytest.raises(ValueError, match="Invalid sort criteria"): + validate_sort(invalid_field, "bids") + with pytest.raises(ValueError, match="Invalid sort criteria"): + validate_sort(invalid_field, "questions") From c132e72ce02b315c74f382c1deeb0f888321861e Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 10 Aug 2023 09:48:57 +0100 Subject: [PATCH 179/208] test: get bids with apikey --- static/swagger_config.yml | 11 +++-------- tests/integration/test_get_bids_with_apikey.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/static/swagger_config.yml b/static/swagger_config.yml index 603c7b3..8d351fc 100644 --- a/static/swagger_config.yml +++ b/static/swagger_config.yml @@ -306,7 +306,7 @@ paths: type: string format: integer security: - - BearerAuth: [] + - ApiKeyAuth: [] responses: '200': description: Successful operation @@ -402,7 +402,7 @@ paths: type: string format: uuid security: - - BearerAuth: [] + - ApiKeyAuth: [] responses: '200': description: A single question @@ -615,7 +615,6 @@ components: description: Summary of feedback type: string example: 'Feedback from client in detail' - # -------------------------------------------- BidLink: description: A link to a bid @@ -686,9 +685,6 @@ components: $ref: '#/components/schemas/SelfQuestionLink' bid: $ref: '#/components/schemas/SelfLink' - - - # -------------------------------------------- QuestionsLink: description: A link to a collection of questions for a bid @@ -704,7 +700,6 @@ components: description: A link to the current resource type: string example: 'http://{hostname}/api/bids/471fea1f-705c-4851-9a5b-df7bc2651428/questions/471fea1f-705c-4851-9a5b-df7bc2651428' - # -------------------------------------------- BidRequestBody: type: object @@ -741,7 +736,7 @@ components: type: array items: $ref: '#/components/schemas/Phase' - +# -------------------------------------------- QuestionRequestBody: type: object required: diff --git a/tests/integration/test_get_bids_with_apikey.py b/tests/integration/test_get_bids_with_apikey.py index 145ca1e..d8e8578 100644 --- a/tests/integration/test_get_bids_with_apikey.py +++ b/tests/integration/test_get_bids_with_apikey.py @@ -1,8 +1,8 @@ -import pytest +# import pytest import requests -@pytest.mark.skip +# @pytest.mark.skip def test_get_bids_with_api_key(): auth_response = requests.get("http://localhost:5000/authorise/") @@ -12,4 +12,10 @@ def test_get_bids_with_api_key(): get_response = requests.get("http://localhost:8080/api/bids", headers=headers) + assert auth_response.status_code == 200 + assert auth_response.json()["API_KEY"] is not None assert get_response.status_code == 200 + # 1 bid out of 10 has "status": "deleted" + assert get_response.json()["total_count"] == 9 + assert get_response.json()["limit"] == 20 + assert get_response.json()["offset"] == 0 From fa4a8dd8b60c4227206c778518bf048a46dab8e3 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 10 Aug 2023 10:01:05 +0100 Subject: [PATCH 180/208] refactor: updated bids test data so was_successful = true for 5 bids --- scripts/test_data/bids.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/test_data/bids.json b/scripts/test_data/bids.json index 43e26b8..7a4764c 100644 --- a/scripts/test_data/bids.json +++ b/scripts/test_data/bids.json @@ -195,7 +195,7 @@ } ], "tender": "Data Governance and Compliance", - "was_successful": false + "was_successful": true }, { "_id": "d9e8f7c6-b5a4-1d3c-7e6f-9b8a0c2d4e5f", @@ -228,7 +228,7 @@ } ], "tender": "Data Infrastructure Modernization", - "was_successful": false + "was_successful": true }, { "_id": "f2e4d6c8-a0b1-42c3-d4e5-f6a7b8c9d0e1", @@ -261,7 +261,7 @@ } ], "tender": "Data Science and Analytics", - "was_successful": false + "was_successful": true }, { "_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6f", @@ -294,7 +294,7 @@ } ], "tender": "Innovative Solutions Development", - "was_successful": false + "was_successful": true }, { "_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4e", @@ -327,6 +327,6 @@ } ], "tender": "Technology Consulting Services", - "was_successful": false + "was_successful": true } ] From 9aa03df1e690716b66daa0a5462dea34a0eb5283 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 23 Aug 2023 10:08:34 +0100 Subject: [PATCH 181/208] fix: changed default value of success in bid model to empty list --- api/models/bid_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/models/bid_model.py b/api/models/bid_model.py index cbc3542..55b0081 100644 --- a/api/models/bid_model.py +++ b/api/models/bid_model.py @@ -34,7 +34,7 @@ def __init__( feedback=None, failed=None, was_successful=False, - success=None, + success=[], status=None, _id=None, links=None, From 19c4f9cb379dbd03b31966578ba8236839fb67c7 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 24 Aug 2023 17:39:47 +0100 Subject: [PATCH 182/208] feat: configured logger, handler and custom json formatter; implemented before_request logging --- TODO.txt | 8 -------- api/controllers/bid_controller.py | 4 ++++ app.py | 26 ++++++++++++++++++++++--- logconfig/custom_formatter.py | 32 +++++++++++++++++++++++++++++++ logconfig/logging_config.json | 23 ++++++++++++++++++++++ 5 files changed, 82 insertions(+), 11 deletions(-) delete mode 100644 TODO.txt create mode 100644 logconfig/custom_formatter.py create mode 100644 logconfig/logging_config.json diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index 1b278d2..0000000 --- a/TODO.txt +++ /dev/null @@ -1,8 +0,0 @@ -- Refactor PUT to retrieve bid with id -- Update bid with user_update -- Load and validate with bid_schema - - will validate unique phases -- Post_load into Bid Model - - will generate timestamp -- 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 5bd050e..31ab89c 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -1,6 +1,7 @@ """ This module implements the bid controller. """ +import logging from datetime import datetime from flask import Blueprint, request, jsonify from marshmallow import ValidationError @@ -25,12 +26,14 @@ ) bid = Blueprint("bid", __name__) +logger = logging.getLogger() @bid.route("/bids", methods=["GET"]) @require_api_key def get_bids(): try: + logger.info("Handling get bids request") hostname = request.headers.get("host") field, order = validate_sort(request.args.get("sort"), "bids") limit, offset = validate_pagination( @@ -58,6 +61,7 @@ def get_bids(): except ValueError as error: return jsonify({"Error": str(error)}), 400 except Exception: + logger.error("Get bids failed", exc_info=True) return showInternalServerError(), 500 diff --git a/app.py b/app.py index 9af4347..f1c292e 100644 --- a/app.py +++ b/app.py @@ -2,14 +2,33 @@ This is a simple Python application. """ - -from flask import Flask +import json +import logging +import logging.config +import traceback +from flask import Flask, request from flask_swagger_ui import get_swaggerui_blueprint from api.controllers.bid_controller import bid from api.controllers.question_controller import question + +# Load the configuration from the JSON file +with open("logconfig/logging_config.json", "r") as f: + config = json.load(f) + +# Configure the logger using dictConfig +logging.config.dictConfig(config) +logger = logging.getLogger() + app = Flask(__name__) + +# Custom middleware to log requests +@app.before_request +def log_request_info(): + logger.info(f"Request: {request.method} {request.url} {request.endpoint}") + + SWAGGER_URL = "/api/docs" # URL for exposing Swagger UI API_URL = "/static/swagger_config.yml" # Our API url @@ -26,4 +45,5 @@ if __name__ == "__main__": - app.run(debug=True, port=8080) + logger.debug("Starting application") + app.run(port=8080) diff --git a/logconfig/custom_formatter.py b/logconfig/custom_formatter.py new file mode 100644 index 0000000..6ef5908 --- /dev/null +++ b/logconfig/custom_formatter.py @@ -0,0 +1,32 @@ +import json, logging, os + +app_name = os.getenv("APP_NAME") +app_version = os.getenv("APP_VERSION") +app_lang = os.getenv("APP_LANG") + + +# Custom JSON Formatter +class CustomJSONFormatter(logging.Formatter): + def format(self, record): + formatted_record = { + "timestamp": self.formatTime(record, self.datefmt), + "level": record.levelname, + "message": record.getMessage(), + "location": "{}:{}:line {}".format( + record.pathname, record.funcName, record.lineno + ), + "span ID": record.process, + "app": {"name": app_name, "version": app_version, "language": app_lang}, + } + + if record.exc_info: + formatted_record["exception"] = { + "error": formatted_record.get( + "exception.error", record.exc_info[1].__class__.__name__ + ), + "traceback": formatted_record.get( + "exception.traceback", self.formatException(record.exc_info) + ), + } + + return json.dumps(formatted_record) diff --git a/logconfig/logging_config.json b/logconfig/logging_config.json new file mode 100644 index 0000000..6a138c7 --- /dev/null +++ b/logconfig/logging_config.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "formatters": { + "JSONFormatter": { + "()": "logconfig.custom_formatter.CustomJSONFormatter" + } + }, + "handlers": { + "fileHandler": { + "class": "logging.FileHandler", + "level": "DEBUG", + "formatter": "JSONFormatter", + "filename": "log.log", + "mode": "a" + } + }, + "loggers": { + "root": { + "level": "DEBUG", + "handlers": ["fileHandler"] + } + } +} From 850c1feeca2101d0a9b4cbf57c8bf37088b66c24 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 25 Aug 2023 12:01:10 +0100 Subject: [PATCH 183/208] feat: implemented loggers in bid controller and helpers --- api/controllers/bid_controller.py | 48 +++++++++++++++++++++----- api/controllers/question_controller.py | 6 ++-- app.py | 11 ++++-- helpers/helpers.py | 14 +++++--- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 31ab89c..d858bef 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -3,9 +3,9 @@ """ import logging from datetime import datetime -from flask import Blueprint, request, jsonify +from flask import Blueprint, g, jsonify, request from marshmallow import ValidationError -from werkzeug.exceptions import UnprocessableEntity +from werkzeug.exceptions import NotFound, UnprocessableEntity from api.models.status_enum import Status from dbconfig.mongo_setup import db from helpers.helpers import ( @@ -33,7 +33,7 @@ @require_api_key def get_bids(): try: - logger.info("Handling get bids request") + logger.info(f"Handling request {g.request_id}") hostname = request.headers.get("host") field, order = validate_sort(request.args.get("sort"), "bids") limit, offset = validate_pagination( @@ -59,9 +59,10 @@ def get_bids(): "items": data, }, 200 except ValueError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return jsonify({"Error": str(error)}), 400 except Exception: - logger.error("Get bids failed", exc_info=True) + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 @@ -69,6 +70,7 @@ def get_bids(): @require_jwt def post_bid(): try: + logger.info(f"Handling request {g.request_id}") # Process input and create data model data = validate_and_create_bid_document(request.get_json()) # Insert document into database collection @@ -76,9 +78,11 @@ def post_bid(): return data, 201 # Return 400 response if input validation fails except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 # Return 500 response in case of connection failure except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 @@ -86,23 +90,29 @@ def post_bid(): @require_api_key def get_bid_by_id(bid_id): try: + logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) data = db["bids"].find_one( {"_id": bid_id, "status": {"$ne": Status.DELETED.value}} ) - # Return 404 response if not found / returns None if not data: - return showNotFoundError(), 404 + raise NotFound("Resource not found") # Get hostname from request headers hostname = request.headers.get("host") # print(data, hostname) data = prepend_host_to_links(data, hostname) return data, 200 + # Return 404 response if not found / returns None + except NotFound: + logger.error(f"{g.request_id} failed", exc_info=True) + return showNotFoundError(), 404 # Return 400 if bid_id is invalid except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 # Return 500 response in case of connection failure except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 @@ -110,6 +120,7 @@ def get_bid_by_id(bid_id): @require_jwt def update_bid_by_id(bid_id): try: + logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) # Retrieve resource where id is equal to bid_id current_bid = db["bids"].find_one( @@ -117,20 +128,26 @@ def update_bid_by_id(bid_id): ) # Return 404 response if not found / returns None if not current_bid: - return showNotFoundError(), 404 + raise NotFound("Resource not found") updated_bid = validate_bid_update(request.get_json(), current_bid) db["bids"].replace_one( {"_id": bid_id}, updated_bid, ) return updated_bid, 200 + except NotFound: + logger.error(f"{g.request_id} failed", exc_info=True) + return showNotFoundError(), 404 # Return 400 response if input validation fails except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 except UnprocessableEntity as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showUnprocessableEntityError(error), 422 # Return 500 response in case of connection failure except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 @@ -138,6 +155,7 @@ def update_bid_by_id(bid_id): @require_admin_access def change_status_to_deleted(bid_id): try: + logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) data = db["bids"].find_one_and_update( {"_id": bid_id, "status": {"$ne": Status.DELETED.value}}, @@ -149,13 +167,18 @@ def change_status_to_deleted(bid_id): }, ) if not data: - return showNotFoundError(), 404 + raise NotFound("Resource not found") return data, 204 + except NotFound: + logger.error(f"{g.request_id} failed", exc_info=True) + return showNotFoundError(), 404 # Return 400 response if input validation fails except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 # Return 500 response in case of connection failure except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 @@ -163,23 +186,30 @@ def change_status_to_deleted(bid_id): @require_admin_access def update_bid_status(bid_id): try: + logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) # 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 current_bid is None: - return showNotFoundError(), 404 + raise NotFound("Resource not found") updated_bid = validate_status_update(request.get_json(), current_bid) db["bids"].replace_one( {"_id": bid_id}, updated_bid, ) return updated_bid, 200 + except NotFound: + logger.error(f"{g.request_id} failed", exc_info=True) + return showNotFoundError(), 404 # Return 400 response if input validation fails except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 except UnprocessableEntity as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showUnprocessableEntityError(error), 422 # Return 500 response in case of connection failure except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 10d12db..badb389 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -1,9 +1,10 @@ """ This module implements the Question Controller blueprint. """ -from flask import Blueprint, request, jsonify +import logging +from flask import Blueprint, g, request, jsonify from marshmallow import ValidationError -from werkzeug.exceptions import UnprocessableEntity +from werkzeug.exceptions import NotFound, UnprocessableEntity from api.models.status_enum import Status from dbconfig.mongo_setup import db from helpers.helpers import ( @@ -23,6 +24,7 @@ ) question = Blueprint("question", __name__) +logger = logging.getLogger() @question.route("/bids//questions", methods=["POST"]) diff --git a/app.py b/app.py index f1c292e..8d596ef 100644 --- a/app.py +++ b/app.py @@ -2,11 +2,12 @@ This is a simple Python application. """ + import json import logging import logging.config -import traceback -from flask import Flask, request +import uuid +from flask import Flask, g, request from flask_swagger_ui import get_swaggerui_blueprint from api.controllers.bid_controller import bid from api.controllers.question_controller import question @@ -26,7 +27,11 @@ # Custom middleware to log requests @app.before_request def log_request_info(): - logger.info(f"Request: {request.method} {request.url} {request.endpoint}") + request_id = uuid.uuid4() + g.request_id = request_id + logger.info( + f"Request: {g.request_id} {request.method} {request.url} {request.endpoint}" + ) SWAGGER_URL = "/api/docs" # URL for exposing Swagger UI diff --git a/helpers/helpers.py b/helpers/helpers.py index 6f7090e..ae43f22 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -1,19 +1,22 @@ """ This module contains helper functions for the API. """ +import jwt +import logging import os import uuid -from functools import wraps from datetime import datetime -import jwt -from jwt.exceptions import InvalidTokenError from dotenv import load_dotenv -from flask import jsonify, request +from flask import g, jsonify, request +from functools import wraps +from jwt.exceptions import InvalidTokenError from werkzeug.exceptions import UnprocessableEntity from api.schemas.bid_schema import BidSchema from api.schemas.id_schema import IdSchema from api.schemas.question_schema import QuestionSchema +logger = logging.getLogger() + def showForbiddenError(): return jsonify({"Error": "Forbidden"}) @@ -102,6 +105,7 @@ def wrapper(*args, **kwargs): api_key = request.headers.get("X-API-Key") assert api_key == os.getenv("API_KEY") except AssertionError: + logger.error(f"{g.request_id} failed", exc_info=True) return showUnauthorizedError(), 401 return fn(*args, **kwargs) @@ -114,6 +118,7 @@ def wrapper(*args, **kwargs): try: validate_token(request=request) except (AssertionError, InvalidTokenError): + logger.error(f"{g.request_id} failed", exc_info=True) return showUnauthorizedError(), 401 return fn(*args, **kwargs) @@ -128,6 +133,7 @@ def wrapper(*args, **kwargs): if decoded["admin"] is False: return showForbiddenError(), 403 except (AssertionError, InvalidTokenError): + logger.error(f"{g.request_id} failed", exc_info=True) return showUnauthorizedError(), 401 return fn(*args, **kwargs) From fe9ca2688e612568d67aec4a9e6e3287e289ac96 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 25 Aug 2023 13:02:22 +0100 Subject: [PATCH 184/208] feat: set werkzeug config to default behaviour print to console not save to file --- api/controllers/question_controller.py | 42 +++++++++++++++++++++++--- logconfig/logging_config.json | 4 +++ tests/conftest.py | 18 ++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index badb389..6578f48 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -31,21 +31,27 @@ @require_jwt def post_question(bid_id): try: + logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) # Check if the bid exists in the database bid = db["bids"].find_one({"_id": bid_id}) if not bid: - return showNotFoundError(), 404 + raise NotFound("Resource not found") # Process input and create data model data = validate_and_create_question_document(request.get_json(), bid_id) # Insert document into database collection db["questions"].insert_one(data) return data, 201 + except NotFound: + logger.error(f"{g.request_id} failed", exc_info=True) + return showNotFoundError(), 404 # Return 400 response if input validation fails except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 # Return 500 response in case of connection failure except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 @@ -53,6 +59,7 @@ def post_question(bid_id): @require_api_key def get_questions(bid_id): try: + logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) hostname = request.headers.get("host") field, order = validate_sort(request.args.get("sort"), "questions") @@ -71,7 +78,7 @@ def get_questions(bid_id): total_count = db["questions"].count_documents(query_filter) if not data: - return showNotFoundError(), 404 + raise NotFound("Resource not found") for question in data: prepend_host_to_links(question, hostname) return { @@ -81,11 +88,17 @@ def get_questions(bid_id): "limit": limit, "items": data, }, 200 + except NotFound: + logger.error(f"{g.request_id} failed", exc_info=True) + return showNotFoundError(), 404 except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 except ValueError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return jsonify({"Error": str(error)}), 400 except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 @@ -93,6 +106,7 @@ def get_questions(bid_id): @require_api_key def get_question(bid_id, question_id): try: + logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) question_id = validate_id_path(question_id) hostname = request.headers.get("host") @@ -104,12 +118,17 @@ def get_question(bid_id, question_id): } ) if not data: - return showNotFoundError(), 404 + raise NotFound("Resource not found") prepend_host_to_links(data, hostname) return data, 200 + except NotFound: + logger.error(f"{g.request_id} failed", exc_info=True) + return showNotFoundError(), 404 except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 @@ -117,16 +136,22 @@ def get_question(bid_id, question_id): @require_admin_access def delete_question(bid_id, question_id): try: + logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) question_id = validate_id_path(question_id) bid = db["bids"].find_one({"_id": bid_id}) if not bid: - return showNotFoundError(), 404 + raise NotFound("Resource not found") data = db["questions"].delete_one({"_id": question_id}) return data.raw_result, 204 + except NotFound: + logger.error(f"{g.request_id} failed", exc_info=True) + return showNotFoundError(), 404 except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 @@ -134,6 +159,7 @@ def delete_question(bid_id, question_id): @require_jwt def update_question(bid_id, question_id): try: + logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) question_id = validate_id_path(question_id) data = db["questions"].find_one( @@ -143,13 +169,19 @@ def update_question(bid_id, question_id): } ) if not data: - return showNotFoundError(), 404 + raise NotFound("Resource not found") updated_question = validate_question_update(request.get_json(), data) db["questions"].replace_one({"_id": question_id}, updated_question) return updated_question, 200 + except NotFound: + logger.error(f"{g.request_id} failed", exc_info=True) + return showNotFoundError(), 404 except ValidationError as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showValidationError(error), 400 except UnprocessableEntity as error: + logger.error(f"{g.request_id} failed", exc_info=True) return showUnprocessableEntityError(error), 422 except Exception: + logger.error(f"{g.request_id} failed", exc_info=True) return showInternalServerError(), 500 diff --git a/logconfig/logging_config.json b/logconfig/logging_config.json index 6a138c7..5f0c673 100644 --- a/logconfig/logging_config.json +++ b/logconfig/logging_config.json @@ -18,6 +18,10 @@ "root": { "level": "DEBUG", "handlers": ["fileHandler"] + }, + "werkzeug": { + "propagate": false, + "qualname": "werkzeug" } } } diff --git a/tests/conftest.py b/tests/conftest.py index 17906da..8a0ed26 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,8 +2,9 @@ This file contains fixtures that are used by multiple tests. """ -import os import jwt +import logging +import os import pytest from app import app from dotenv import load_dotenv @@ -11,6 +12,21 @@ load_dotenv() +@pytest.fixture(scope="session", autouse=True) +def disable_logging_before_tests(request): + """Fixture to disable logging before running tests.""" + logging.disable(logging.CRITICAL) + yield + logging.disable(logging.NOTSET) + + +@pytest.fixture(autouse=True) +def enable_logging_after_tests(request): + """Fixture to re-enable logging after tests are completed.""" + yield + logging.disable(logging.NOTSET) + + @pytest.fixture(scope="session") def test_client(): os.environ["TEST_ENVIRONMENT"] = "True" From 4f0138424d1fbde43364b624c1a7ba384dd58290 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 25 Aug 2023 13:04:47 +0100 Subject: [PATCH 185/208] feat: added name to formatted log; removed wip test config to disable logging during unit tests --- logconfig/custom_formatter.py | 1 + tests/conftest.py | 15 --------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/logconfig/custom_formatter.py b/logconfig/custom_formatter.py index 6ef5908..d775d40 100644 --- a/logconfig/custom_formatter.py +++ b/logconfig/custom_formatter.py @@ -11,6 +11,7 @@ def format(self, record): formatted_record = { "timestamp": self.formatTime(record, self.datefmt), "level": record.levelname, + "name": record.name, "message": record.getMessage(), "location": "{}:{}:line {}".format( record.pathname, record.funcName, record.lineno diff --git a/tests/conftest.py b/tests/conftest.py index 8a0ed26..d59f816 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,21 +12,6 @@ load_dotenv() -@pytest.fixture(scope="session", autouse=True) -def disable_logging_before_tests(request): - """Fixture to disable logging before running tests.""" - logging.disable(logging.CRITICAL) - yield - logging.disable(logging.NOTSET) - - -@pytest.fixture(autouse=True) -def enable_logging_after_tests(request): - """Fixture to re-enable logging after tests are completed.""" - yield - logging.disable(logging.NOTSET) - - @pytest.fixture(scope="session") def test_client(): os.environ["TEST_ENVIRONMENT"] = "True" From 4bdfe57e0cc3a3e7053b43e0e48f6ff9ab3ee26c Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 25 Aug 2023 14:10:53 +0100 Subject: [PATCH 186/208] refactor: removed name from structured log; refactored new request log for readability --- app.py | 2 +- logconfig/custom_formatter.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app.py b/app.py index 8d596ef..0a87234 100644 --- a/app.py +++ b/app.py @@ -30,7 +30,7 @@ def log_request_info(): request_id = uuid.uuid4() g.request_id = request_id logger.info( - f"Request: {g.request_id} {request.method} {request.url} {request.endpoint}" + f"New request {g.request_id}: {request.method} {request.url} - - {request.endpoint}" ) diff --git a/logconfig/custom_formatter.py b/logconfig/custom_formatter.py index d775d40..6ef5908 100644 --- a/logconfig/custom_formatter.py +++ b/logconfig/custom_formatter.py @@ -11,7 +11,6 @@ def format(self, record): formatted_record = { "timestamp": self.formatTime(record, self.datefmt), "level": record.levelname, - "name": record.name, "message": record.getMessage(), "location": "{}:{}:line {}".format( record.pathname, record.funcName, record.lineno From 32e75bfa4d230e3c51b9306e97b1ff6942e5f7aa Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 25 Aug 2023 14:38:59 +0100 Subject: [PATCH 187/208] fix: disabled logging during unit tests; ignored custom_formatter file during unit tests --- Makefile | 2 +- tests/conftest.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6379bac..fb57d58 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ swag: test: -coverage run -m pytest -vv @echo "TEST COVERAGE REPORT" - coverage report -m --omit="app.py,tests/*,dbconfig/*" + coverage report -m --omit="app.py,tests/*,dbconfig/*,custom_formatter.py" test-setup: @echo "Setting up test database..." diff --git a/tests/conftest.py b/tests/conftest.py index d59f816..1c91da7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,13 @@ load_dotenv() +@pytest.fixture(autouse=True) +def before_and_after_test(): + logging.disable(logging.CRITICAL) + yield + logging.disable(logging.NOTSET) + + @pytest.fixture(scope="session") def test_client(): os.environ["TEST_ENVIRONMENT"] = "True" From 1af531a9a1f65760179c73adfca986378659f600 Mon Sep 17 00:00:00 2001 From: Lakorthus Date: Fri, 25 Aug 2023 15:19:06 +0100 Subject: [PATCH 188/208] refactor: formated traceback for readability --- logconfig/custom_formatter.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/logconfig/custom_formatter.py b/logconfig/custom_formatter.py index 6ef5908..f1af1b1 100644 --- a/logconfig/custom_formatter.py +++ b/logconfig/custom_formatter.py @@ -7,6 +7,16 @@ # Custom JSON Formatter class CustomJSONFormatter(logging.Formatter): + def format_traceback(self, exc_info): + _, exception_value, tb = exc_info + traceback_info = { + "path": tb.tb_frame.f_code.co_filename, + "line": tb.tb_lineno, + "location": tb.tb_frame.f_code.co_name, + "error": str(exception_value), + } + return traceback_info + def format(self, record): formatted_record = { "timestamp": self.formatTime(record, self.datefmt), @@ -20,13 +30,7 @@ def format(self, record): } if record.exc_info: - formatted_record["exception"] = { - "error": formatted_record.get( - "exception.error", record.exc_info[1].__class__.__name__ - ), - "traceback": formatted_record.get( - "exception.traceback", self.formatException(record.exc_info) - ), - } + traceback_info = self.format_traceback(record.exc_info) + formatted_record["exception"] = traceback_info - return json.dumps(formatted_record) + return json.dumps(formatted_record, default=str) From fe4d6465418d19f82377ee4fbe8e281537b0ba7d Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 25 Aug 2023 15:49:14 +0100 Subject: [PATCH 189/208] feat: finalised formatting for structured logging; refactored helpers for meaningful error messages --- helpers/helpers.py | 23 +++++++++++++---------- logconfig/custom_formatter.py | 4 ++-- request_examples/delete_bid.http | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/helpers/helpers.py b/helpers/helpers.py index ae43f22..8654ef7 100644 --- a/helpers/helpers.py +++ b/helpers/helpers.py @@ -99,22 +99,22 @@ def prepend_host_to_links(resource, hostname): def require_api_key(fn): @wraps(fn) - def wrapper(*args, **kwargs): + def validate_api_key(*args, **kwargs): try: load_dotenv() api_key = request.headers.get("X-API-Key") - assert api_key == os.getenv("API_KEY") + assert api_key == os.getenv("API_KEY"), "API Key is not valid" except AssertionError: logger.error(f"{g.request_id} failed", exc_info=True) return showUnauthorizedError(), 401 return fn(*args, **kwargs) - return wrapper + return validate_api_key def require_jwt(fn): @wraps(fn) - def wrapper(*args, **kwargs): + def validate_jwt(*args, **kwargs): try: validate_token(request=request) except (AssertionError, InvalidTokenError): @@ -122,29 +122,32 @@ def wrapper(*args, **kwargs): return showUnauthorizedError(), 401 return fn(*args, **kwargs) - return wrapper + return validate_jwt def require_admin_access(fn): @wraps(fn) - def wrapper(*args, **kwargs): + def validate_admin(*args, **kwargs): try: decoded = validate_token(request=request) if decoded["admin"] is False: - return showForbiddenError(), 403 + raise PermissionError("Forbidden") + except PermissionError: + logger.error(f"{g.request_id} failed", exc_info=True) + return showForbiddenError(), 403 except (AssertionError, InvalidTokenError): logger.error(f"{g.request_id} failed", exc_info=True) return showUnauthorizedError(), 401 return fn(*args, **kwargs) - return wrapper + return validate_admin def validate_token(request): prefix = "Bearer " auth_header = request.headers.get("Authorization") - assert auth_header is not None - assert auth_header.startswith(prefix) is True + assert auth_header is not None, "Not authorized" + assert auth_header.startswith(prefix) is True, "Not authorized" token = auth_header[len(prefix) :] load_dotenv() key = os.getenv("SECRET_KEY") diff --git a/logconfig/custom_formatter.py b/logconfig/custom_formatter.py index f1af1b1..9d0a43a 100644 --- a/logconfig/custom_formatter.py +++ b/logconfig/custom_formatter.py @@ -10,9 +10,9 @@ class CustomJSONFormatter(logging.Formatter): def format_traceback(self, exc_info): _, exception_value, tb = exc_info traceback_info = { - "path": tb.tb_frame.f_code.co_filename, + "exc_location": tb.tb_frame.f_code.co_filename, "line": tb.tb_lineno, - "location": tb.tb_frame.f_code.co_name, + "function": tb.tb_frame.f_code.co_name, "error": str(exception_value), } return traceback_info diff --git a/request_examples/delete_bid.http b/request_examples/delete_bid.http index 1d84f07..dfe4139 100644 --- a/request_examples/delete_bid.http +++ b/request_examples/delete_bid.http @@ -1,2 +1,2 @@ DELETE http://localhost:8080/api/bids/b4846631-9135-4208-8e37-70eba8f77e15 HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRlc3RlciBNY1Rlc3RmYWNlIiwiYWRtaW4iOmZhbHNlfQ.Dg7f8LVtALYWvjZH31re5C-Pc6Hp6Ra-U4LAy0ZQQ9M +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlVzZXIiLCJhZG1pbiI6ZmFsc2UsImV4cCI6MTY5MzU3ODU5MH0.KD5xKvfynSjG3VNlH54g2AkceQxtkysxxTM5GaTnnXt From d422f4f908a61b74f1038375fbba5872e1d58dc1 Mon Sep 17 00:00:00 2001 From: piratejas Date: Tue, 29 Aug 2023 14:16:06 +0100 Subject: [PATCH 190/208] docs: updated readme to include app info --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4d7233..91cf88c 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,10 @@ To do this, create a `.env` file in your root folder, with the following key/val MAX_OFFSET=2000 MAX_LIMIT=1000 DEFAULT_SORT_BIDS=bid_date - DEFAULT_SORT_QUESTIONS=description + DEFAULT_SORT_QUESTIONS=description + APP_NAME=BidsAPI + APP_VERSION=0.6.0 + APP_LANG=Python -------------- From d3d57060d65402f45b9f2758823b1051de1abddb Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 13 Sep 2023 14:55:04 +0100 Subject: [PATCH 191/208] test: renamed pause_logging fixture; added print statements before and after; added -s tag to make target --- Makefile | 2 +- tests/conftest.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fb57d58..1a0f7ec 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ swag: open http://localhost:8080/api/docs/#/ test: - -coverage run -m pytest -vv + -coverage run -m pytest -vv -s @echo "TEST COVERAGE REPORT" coverage report -m --omit="app.py,tests/*,dbconfig/*,custom_formatter.py" diff --git a/tests/conftest.py b/tests/conftest.py index 1c91da7..b9d8b31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,10 +13,17 @@ @pytest.fixture(autouse=True) -def before_and_after_test(): +def integration_setup_and_teardown(): + pass + + +@pytest.fixture(autouse=True) +def pause_logging(): logging.disable(logging.CRITICAL) + print("----------Logging disabled----------") yield logging.disable(logging.NOTSET) + print("----------Logging re-enabled----------") @pytest.fixture(scope="session") From a4b7649926db7092af0ad6df569d8b7d8ae1fed0 Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 13 Sep 2023 16:17:12 +0100 Subject: [PATCH 192/208] fix: moved conftest.py to root folder --- Makefile | 2 +- tests/conftest.py => conftest.py | 0 tests/integration/test_get_bids_with_apikey.py | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename tests/conftest.py => conftest.py (100%) diff --git a/Makefile b/Makefile index 1a0f7ec..6f70273 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ swag: open http://localhost:8080/api/docs/#/ test: - -coverage run -m pytest -vv -s + -coverage run -m pytest -k "not integration" -vv -s @echo "TEST COVERAGE REPORT" coverage report -m --omit="app.py,tests/*,dbconfig/*,custom_formatter.py" diff --git a/tests/conftest.py b/conftest.py similarity index 100% rename from tests/conftest.py rename to conftest.py diff --git a/tests/integration/test_get_bids_with_apikey.py b/tests/integration/test_get_bids_with_apikey.py index d8e8578..0633350 100644 --- a/tests/integration/test_get_bids_with_apikey.py +++ b/tests/integration/test_get_bids_with_apikey.py @@ -1,8 +1,8 @@ -# import pytest +import pytest import requests -# @pytest.mark.skip +@pytest.mark.integration def test_get_bids_with_api_key(): auth_response = requests.get("http://localhost:5000/authorise/") From 65371bacc691ec4b8378f2cb2ec3f485df997bab Mon Sep 17 00:00:00 2001 From: piratejas Date: Wed, 13 Sep 2023 17:20:24 +0100 Subject: [PATCH 193/208] test: wip - setup/teardown working; TODO api request needs to query test db not actual --- Makefile | 11 ++++ conftest.py | 60 ++++++++++++++++++- .../integration/test_get_bids_with_apikey.py | 18 ++---- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 6f70273..8d8ac52 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,17 @@ test: @echo "TEST COVERAGE REPORT" coverage report -m --omit="app.py,tests/*,dbconfig/*,custom_formatter.py" +test-dbclean: + @echo "Cleaning up database..." + export TEST_ENVIRONMENT=true; \ + cd ./scripts/; \ + make dbclean; \ + export TEST_ENVIRONMENT= + @echo "Database cleared." + +test-integration: + pytest -m integration + test-setup: @echo "Setting up test database..." export TEST_ENVIRONMENT=true; \ diff --git a/conftest.py b/conftest.py index b9d8b31..49c4d89 100644 --- a/conftest.py +++ b/conftest.py @@ -8,13 +8,67 @@ import pytest from app import app from dotenv import load_dotenv +from pymongo import MongoClient load_dotenv() - -@pytest.fixture(autouse=True) +DB_HOST = os.getenv("DB_HOST") +DB_PORT = 27017 +DB_NAME = os.getenv("TEST_DB_NAME") +test_data = [ + { + "_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": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + "self": "/api/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": "a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7", + "alias": "ACME", + "bid_date": "2023-07-05", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "ACME Corporation", + "failed": {"has_score": True, "out_of": 36, "phase": 2, "score": 15}, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", + }, + "last_updated": "2023-08-01T14:30:20.123456", + "links": { + "questions": "/api/bids/a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7/questions", + "self": "/api/bids/a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7", + }, + "status": "in_progress", + "success": [{"has_score": True, "out_of": 36, "phase": 1, "score": 28}], + "tender": "Data Analytics Solution", + "was_successful": False, + }, +] + + +@pytest.fixture def integration_setup_and_teardown(): - pass + client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) + data_base = client[DB_NAME] + collection = data_base["bids"] + collection.insert_many(test_data) + yield + collection.delete_many({}) @pytest.fixture(autouse=True) diff --git a/tests/integration/test_get_bids_with_apikey.py b/tests/integration/test_get_bids_with_apikey.py index 0633350..62ca40d 100644 --- a/tests/integration/test_get_bids_with_apikey.py +++ b/tests/integration/test_get_bids_with_apikey.py @@ -2,20 +2,14 @@ import requests -@pytest.mark.integration -def test_get_bids_with_api_key(): - auth_response = requests.get("http://localhost:5000/authorise/") +pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") - api_key = auth_response.json()["API_KEY"] +@pytest.mark.integration +def test_get_bids_with_api_key(api_key): headers = {"Content-Type": "application/json", "X-API-Key": api_key} - get_response = requests.get("http://localhost:8080/api/bids", headers=headers) + response = requests.get("http://localhost:8080/api/bids", headers=headers) - assert auth_response.status_code == 200 - assert auth_response.json()["API_KEY"] is not None - assert get_response.status_code == 200 - # 1 bid out of 10 has "status": "deleted" - assert get_response.json()["total_count"] == 9 - assert get_response.json()["limit"] == 20 - assert get_response.json()["offset"] == 0 + assert response.status_code == 200 + assert len(response.json()["items"]) == 2 From fd3d86dd2131cb0fa6c7dc627eb15d5b48921e84 Mon Sep 17 00:00:00 2001 From: piratejas Date: Thu, 14 Sep 2023 13:24:37 +0100 Subject: [PATCH 194/208] refactor: moved unit tests into tests/unit folder --- Makefile | 2 +- app.py | 2 +- conftest.py | 7 ++++++- pytest.ini | 3 +++ tests/integration/test_get_bids_with_apikey.py | 16 +++++++++++----- tests/{ => unit}/test_bid_schema.py | 0 tests/{ => unit}/test_delete_bid.py | 0 tests/{ => unit}/test_delete_question.py | 0 tests/{ => unit}/test_get_bid_by_id.py | 0 tests/{ => unit}/test_get_bids.py | 0 tests/{ => unit}/test_get_question_by_id.py | 0 tests/{ => unit}/test_get_questions.py | 0 tests/{ => unit}/test_helpers.py | 0 tests/{ => unit}/test_phase_schema.py | 0 tests/{ => unit}/test_post_bid.py | 0 tests/{ => unit}/test_post_question.py | 0 tests/{ => unit}/test_question_schema.py | 0 tests/{ => unit}/test_update_bid_by_id.py | 0 tests/{ => unit}/test_update_bid_status.py | 0 tests/{ => unit}/test_update_question.py | 0 20 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 pytest.ini rename tests/{ => unit}/test_bid_schema.py (100%) rename tests/{ => unit}/test_delete_bid.py (100%) rename tests/{ => unit}/test_delete_question.py (100%) rename tests/{ => unit}/test_get_bid_by_id.py (100%) rename tests/{ => unit}/test_get_bids.py (100%) rename tests/{ => unit}/test_get_question_by_id.py (100%) rename tests/{ => unit}/test_get_questions.py (100%) rename tests/{ => unit}/test_helpers.py (100%) rename tests/{ => unit}/test_phase_schema.py (100%) rename tests/{ => unit}/test_post_bid.py (100%) rename tests/{ => unit}/test_post_question.py (100%) rename tests/{ => unit}/test_question_schema.py (100%) rename tests/{ => unit}/test_update_bid_by_id.py (100%) rename tests/{ => unit}/test_update_bid_status.py (100%) rename tests/{ => unit}/test_update_question.py (100%) diff --git a/Makefile b/Makefile index 8d8ac52..24110e2 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ swag: test: -coverage run -m pytest -k "not integration" -vv -s @echo "TEST COVERAGE REPORT" - coverage report -m --omit="app.py,tests/*,dbconfig/*,custom_formatter.py" + coverage report -m --omit="app.py,tests/*,dbconfig/*,custom_formatter.py,conftest.py" test-dbclean: @echo "Cleaning up database..." diff --git a/app.py b/app.py index 0a87234..221e192 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,7 @@ """ -import json +import json, os import logging import logging.config import uuid diff --git a/conftest.py b/conftest.py index 49c4d89..e083734 100644 --- a/conftest.py +++ b/conftest.py @@ -14,7 +14,11 @@ DB_HOST = os.getenv("DB_HOST") DB_PORT = 27017 -DB_NAME = os.getenv("TEST_DB_NAME") +DB_NAME = os.getenv("DB_NAME") + +if os.environ.get("TEST_ENVIRONMENT"): + DB_NAME = os.getenv("TEST_DB_NAME") + test_data = [ { "_id": "be15c306-c85b-4e67-a9f6-682553c065a1", @@ -69,6 +73,7 @@ def integration_setup_and_teardown(): collection.insert_many(test_data) yield collection.delete_many({}) + # print("AFTER") @pytest.fixture(autouse=True) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..bed64ef --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +markers = + integration \ No newline at end of file diff --git a/tests/integration/test_get_bids_with_apikey.py b/tests/integration/test_get_bids_with_apikey.py index 62ca40d..31a6634 100644 --- a/tests/integration/test_get_bids_with_apikey.py +++ b/tests/integration/test_get_bids_with_apikey.py @@ -1,4 +1,4 @@ -import pytest +import pytest, os import requests @@ -6,10 +6,16 @@ @pytest.mark.integration -def test_get_bids_with_api_key(api_key): - headers = {"Content-Type": "application/json", "X-API-Key": api_key} +def test_get_bids_with_api_key(test_client, api_key): + headers = { + "host": "localhost:8080", + "Content-Type": "application/json", + "X-API-Key": api_key, + } - response = requests.get("http://localhost:8080/api/bids", headers=headers) + response = test_client.get("/api/bids", headers=headers) + env = os.environ.get("TEST_ENVIRONMENT") + print("This", env) assert response.status_code == 200 - assert len(response.json()["items"]) == 2 + assert len(response.get_json()["items"]) == 2 diff --git a/tests/test_bid_schema.py b/tests/unit/test_bid_schema.py similarity index 100% rename from tests/test_bid_schema.py rename to tests/unit/test_bid_schema.py diff --git a/tests/test_delete_bid.py b/tests/unit/test_delete_bid.py similarity index 100% rename from tests/test_delete_bid.py rename to tests/unit/test_delete_bid.py diff --git a/tests/test_delete_question.py b/tests/unit/test_delete_question.py similarity index 100% rename from tests/test_delete_question.py rename to tests/unit/test_delete_question.py diff --git a/tests/test_get_bid_by_id.py b/tests/unit/test_get_bid_by_id.py similarity index 100% rename from tests/test_get_bid_by_id.py rename to tests/unit/test_get_bid_by_id.py diff --git a/tests/test_get_bids.py b/tests/unit/test_get_bids.py similarity index 100% rename from tests/test_get_bids.py rename to tests/unit/test_get_bids.py diff --git a/tests/test_get_question_by_id.py b/tests/unit/test_get_question_by_id.py similarity index 100% rename from tests/test_get_question_by_id.py rename to tests/unit/test_get_question_by_id.py diff --git a/tests/test_get_questions.py b/tests/unit/test_get_questions.py similarity index 100% rename from tests/test_get_questions.py rename to tests/unit/test_get_questions.py diff --git a/tests/test_helpers.py b/tests/unit/test_helpers.py similarity index 100% rename from tests/test_helpers.py rename to tests/unit/test_helpers.py diff --git a/tests/test_phase_schema.py b/tests/unit/test_phase_schema.py similarity index 100% rename from tests/test_phase_schema.py rename to tests/unit/test_phase_schema.py diff --git a/tests/test_post_bid.py b/tests/unit/test_post_bid.py similarity index 100% rename from tests/test_post_bid.py rename to tests/unit/test_post_bid.py diff --git a/tests/test_post_question.py b/tests/unit/test_post_question.py similarity index 100% rename from tests/test_post_question.py rename to tests/unit/test_post_question.py diff --git a/tests/test_question_schema.py b/tests/unit/test_question_schema.py similarity index 100% rename from tests/test_question_schema.py rename to tests/unit/test_question_schema.py diff --git a/tests/test_update_bid_by_id.py b/tests/unit/test_update_bid_by_id.py similarity index 100% rename from tests/test_update_bid_by_id.py rename to tests/unit/test_update_bid_by_id.py diff --git a/tests/test_update_bid_status.py b/tests/unit/test_update_bid_status.py similarity index 100% rename from tests/test_update_bid_status.py rename to tests/unit/test_update_bid_status.py diff --git a/tests/test_update_question.py b/tests/unit/test_update_question.py similarity index 100% rename from tests/test_update_question.py rename to tests/unit/test_update_question.py From 4a911a767a4f600e2187c13e59ff1eeb816a5873 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 15 Sep 2023 14:22:26 +0100 Subject: [PATCH 195/208] test: changed mongo setup to create client when app is created with environmental variables from config file; changed conftest for setup and teardown; refactored controllers to use db instance in current_app context; TODO change unit tests to mock db from context --- Makefile | 12 ++-- api/controllers/bid_controller.py | 10 +++- api/controllers/question_controller.py | 8 ++- app.py | 55 +++++++++++++------ config.py | 19 +++++++ conftest.py | 37 ++++++------- dbconfig/mongo_setup.py | 18 +++--- scripts/create_bids.py | 2 +- scripts/create_questions.py | 2 +- scripts/delete_bids.py | 2 +- scripts/delete_questions.py | 2 +- .../integration/test_get_bids_with_apikey.py | 4 +- 12 files changed, 110 insertions(+), 61 deletions(-) create mode 100644 config.py diff --git a/Makefile b/Makefile index 24110e2..1d00bee 100644 --- a/Makefile +++ b/Makefile @@ -66,29 +66,29 @@ swag: open http://localhost:8080/api/docs/#/ test: - -coverage run -m pytest -k "not integration" -vv -s + -coverage run -m pytest tests/unit -vv -s @echo "TEST COVERAGE REPORT" coverage report -m --omit="app.py,tests/*,dbconfig/*,custom_formatter.py,conftest.py" test-dbclean: @echo "Cleaning up database..." - export TEST_ENVIRONMENT=true; \ + export TESTING=true; \ cd ./scripts/; \ make dbclean; \ - export TEST_ENVIRONMENT= + export TESTING= @echo "Database cleared." test-integration: - pytest -m integration + pytest tests/integration -vv -s test-setup: @echo "Setting up test database..." - export TEST_ENVIRONMENT=true; \ + export TESTING=true; \ cd ./scripts/; \ make dbclean; \ make bids; \ make questions; \ - export TEST_ENVIRONMENT= + export TESTING= @echo "Test database setup complete." .PHONY: helptools authplay branch check commit format lint diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index d858bef..28208b4 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -3,11 +3,10 @@ """ import logging from datetime import datetime -from flask import Blueprint, g, jsonify, request +from flask import Blueprint, current_app, g, jsonify, request from marshmallow import ValidationError from werkzeug.exceptions import NotFound, UnprocessableEntity from api.models.status_enum import Status -from dbconfig.mongo_setup import db from helpers.helpers import ( showInternalServerError, showNotFoundError, @@ -33,6 +32,8 @@ @require_api_key def get_bids(): try: + # print(current_app.db) + db = current_app.db logger.info(f"Handling request {g.request_id}") hostname = request.headers.get("host") field, order = validate_sort(request.args.get("sort"), "bids") @@ -70,6 +71,7 @@ def get_bids(): @require_jwt def post_bid(): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") # Process input and create data model data = validate_and_create_bid_document(request.get_json()) @@ -90,6 +92,7 @@ def post_bid(): @require_api_key def get_bid_by_id(bid_id): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) data = db["bids"].find_one( @@ -120,6 +123,7 @@ def get_bid_by_id(bid_id): @require_jwt def update_bid_by_id(bid_id): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) # Retrieve resource where id is equal to bid_id @@ -155,6 +159,7 @@ def update_bid_by_id(bid_id): @require_admin_access def change_status_to_deleted(bid_id): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) data = db["bids"].find_one_and_update( @@ -186,6 +191,7 @@ def change_status_to_deleted(bid_id): @require_admin_access def update_bid_status(bid_id): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) # Retrieve resource where id is equal to bid_id diff --git a/api/controllers/question_controller.py b/api/controllers/question_controller.py index 6578f48..68084cd 100644 --- a/api/controllers/question_controller.py +++ b/api/controllers/question_controller.py @@ -2,11 +2,10 @@ This module implements the Question Controller blueprint. """ import logging -from flask import Blueprint, g, request, jsonify +from flask import Blueprint, current_app, g, request, jsonify from marshmallow import ValidationError from werkzeug.exceptions import NotFound, UnprocessableEntity from api.models.status_enum import Status -from dbconfig.mongo_setup import db from helpers.helpers import ( showInternalServerError, showNotFoundError, @@ -31,6 +30,7 @@ @require_jwt def post_question(bid_id): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) # Check if the bid exists in the database @@ -59,6 +59,7 @@ def post_question(bid_id): @require_api_key def get_questions(bid_id): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) hostname = request.headers.get("host") @@ -106,6 +107,7 @@ def get_questions(bid_id): @require_api_key def get_question(bid_id, question_id): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) question_id = validate_id_path(question_id) @@ -136,6 +138,7 @@ def get_question(bid_id, question_id): @require_admin_access def delete_question(bid_id, question_id): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) question_id = validate_id_path(question_id) @@ -159,6 +162,7 @@ def delete_question(bid_id, question_id): @require_jwt def update_question(bid_id, question_id): try: + db = current_app.db logger.info(f"Handling request {g.request_id}") bid_id = validate_id_path(bid_id) question_id = validate_id_path(question_id) diff --git a/app.py b/app.py index 221e192..4904282 100644 --- a/app.py +++ b/app.py @@ -3,14 +3,20 @@ """ -import json, os +import json import logging import logging.config +import os import uuid +from dotenv import load_dotenv from flask import Flask, g, request from flask_swagger_ui import get_swaggerui_blueprint from api.controllers.bid_controller import bid from api.controllers.question_controller import question +from dbconfig.mongo_setup import get_db + + +load_dotenv() # Load the configuration from the JSON file @@ -21,19 +27,8 @@ logging.config.dictConfig(config) logger = logging.getLogger() -app = Flask(__name__) - - -# Custom middleware to log requests -@app.before_request -def log_request_info(): - request_id = uuid.uuid4() - g.request_id = request_id - logger.info( - f"New request {g.request_id}: {request.method} {request.url} - - {request.endpoint}" - ) - +# Swagger config SWAGGER_URL = "/api/docs" # URL for exposing Swagger UI API_URL = "/static/swagger_config.yml" # Our API url @@ -44,11 +39,39 @@ def log_request_info(): config={"app_name": "Bids API Swagger"}, ) -app.register_blueprint(swaggerui_blueprint) -app.register_blueprint(bid, url_prefix="/api") -app.register_blueprint(question, url_prefix="/api") + +def create_app(): + # Create the Flask application + app = Flask(__name__) + + # Configure the Flask application + config_type = os.getenv("CONFIG_TYPE", default="config.DevelopmentConfig") + app.config.from_object(config_type) + + # Register blueprints + app.register_blueprint(swaggerui_blueprint) + app.register_blueprint(bid, url_prefix="/api") + app.register_blueprint(question, url_prefix="/api") + + # Create new client and connect to server in application context + with app.app_context(): + db = get_db(app.config["DB_HOST"], app.config["DB_PORT"], app.config["DB_NAME"]) + app.db = db + + # Custom middleware to log request info + def log_request_info(): + request_id = uuid.uuid4() + g.request_id = request_id + logger.info( + f"New request {g.request_id}: {request.method} {request.url} - - {request.endpoint}" + ) + + app.before_request(log_request_info) + + return app if __name__ == "__main__": logger.debug("Starting application") + app = create_app() app.run(port=8080) diff --git a/config.py b/config.py new file mode 100644 index 0000000..86233ec --- /dev/null +++ b/config.py @@ -0,0 +1,19 @@ +import os + + +class Config(object): + FLASK_ENV = "development" + DEBUG = False + TESTING = False + DB_HOST = os.getenv("DB_HOST") + DB_PORT = 27017 + DB_NAME = os.getenv("DB_NAME") + + +class DevelopmentConfig(Config): + DEBUG = True + + +class TestingConfig(Config): + TESTING = True + DB_NAME = os.getenv("TEST_DB_NAME") diff --git a/conftest.py b/conftest.py index e083734..cb406f0 100644 --- a/conftest.py +++ b/conftest.py @@ -6,18 +6,12 @@ import logging import os import pytest -from app import app +from app import create_app from dotenv import load_dotenv -from pymongo import MongoClient +from dbconfig.mongo_setup import get_db load_dotenv() -DB_HOST = os.getenv("DB_HOST") -DB_PORT = 27017 -DB_NAME = os.getenv("DB_NAME") - -if os.environ.get("TEST_ENVIRONMENT"): - DB_NAME = os.getenv("TEST_DB_NAME") test_data = [ { @@ -66,16 +60,27 @@ @pytest.fixture -def integration_setup_and_teardown(): - client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) - data_base = client[DB_NAME] - collection = data_base["bids"] +def integration_setup_and_teardown(test_app): + db = test_app.db + collection = db["bids"] collection.insert_many(test_data) yield collection.delete_many({}) # print("AFTER") +@pytest.fixture(scope="session") +def test_app(): + os.environ["CONFIG_TYPE"] = "config.TestingConfig" + app = create_app() + yield app + + +@pytest.fixture(scope="session") +def test_client(test_app): + return test_app.test_client() + + @pytest.fixture(autouse=True) def pause_logging(): logging.disable(logging.CRITICAL) @@ -85,14 +90,6 @@ def pause_logging(): print("----------Logging re-enabled----------") -@pytest.fixture(scope="session") -def test_client(): - os.environ["TEST_ENVIRONMENT"] = "True" - with app.test_client() as client: - yield client - os.environ.pop("TEST_ENVIRONMENT") - - @pytest.fixture(scope="session") def api_key(): api_key = os.getenv("API_KEY") diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index ad7619d..a5fa9a8 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -8,13 +8,15 @@ load_dotenv() -DB_HOST = os.getenv("DB_HOST") -DB_PORT = 27017 -DB_NAME = os.getenv("DB_NAME") -if os.environ.get("TEST_ENVIRONMENT"): - DB_NAME = os.getenv("TEST_DB_NAME") +def get_db(DB_HOST, DB_PORT, DB_NAME): + # DB_HOST = os.getenv("DB_HOST") + # DB_PORT = 27017 + # DB_NAME = os.getenv("DB_NAME") -# Create a new client and connect to the server -client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) -db = client[DB_NAME] + # if os.environ.get("TESTING"): + # DB_NAME = os.getenv("TEST_DB_NAME") + + # Create a new client and connect to the server + client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) + return client[DB_NAME] diff --git a/scripts/create_bids.py b/scripts/create_bids.py index e784364..2e00c46 100644 --- a/scripts/create_bids.py +++ b/scripts/create_bids.py @@ -17,7 +17,7 @@ DB_PORT = 27017 DB_NAME = os.getenv("DB_NAME") -if os.environ.get("TEST_ENVIRONMENT"): +if os.environ.get("TESTING"): DB_NAME = os.getenv("TEST_DB_NAME") diff --git a/scripts/create_questions.py b/scripts/create_questions.py index 63089a6..c921b0e 100644 --- a/scripts/create_questions.py +++ b/scripts/create_questions.py @@ -19,7 +19,7 @@ DB_PORT = 27017 DB_NAME = os.getenv("DB_NAME") -if os.environ.get("TEST_ENVIRONMENT"): +if os.environ.get("TESTING"): DB_NAME = os.getenv("TEST_DB_NAME") diff --git a/scripts/delete_bids.py b/scripts/delete_bids.py index 2f7f00d..1a3c3df 100644 --- a/scripts/delete_bids.py +++ b/scripts/delete_bids.py @@ -15,7 +15,7 @@ DB_PORT = 27017 DB_NAME = os.getenv("DB_NAME") -if os.environ.get("TEST_ENVIRONMENT"): +if os.environ.get("TESTING"): DB_NAME = os.getenv("TEST_DB_NAME") diff --git a/scripts/delete_questions.py b/scripts/delete_questions.py index 532ee9e..37f09c0 100644 --- a/scripts/delete_questions.py +++ b/scripts/delete_questions.py @@ -15,7 +15,7 @@ DB_PORT = 27017 DB_NAME = os.getenv("DB_NAME") -if os.environ.get("TEST_ENVIRONMENT"): +if os.environ.get("TESTING"): DB_NAME = os.getenv("TEST_DB_NAME") diff --git a/tests/integration/test_get_bids_with_apikey.py b/tests/integration/test_get_bids_with_apikey.py index 31a6634..b7cfd2a 100644 --- a/tests/integration/test_get_bids_with_apikey.py +++ b/tests/integration/test_get_bids_with_apikey.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") -@pytest.mark.integration +# @pytest.mark.integration def test_get_bids_with_api_key(test_client, api_key): headers = { "host": "localhost:8080", @@ -15,7 +15,5 @@ def test_get_bids_with_api_key(test_client, api_key): response = test_client.get("/api/bids", headers=headers) - env = os.environ.get("TEST_ENVIRONMENT") - print("This", env) assert response.status_code == 200 assert len(response.get_json()["items"]) == 2 From 4cb5c0fa0d4de716bf3d5f1870d0741652f81da1 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 15 Sep 2023 14:58:45 +0100 Subject: [PATCH 196/208] refactor: separated integ test data into json file --- Makefile | 4 +- conftest.py | 55 ++++----------------------- dbconfig/mongo_setup.py | 7 ---- tests/integration/data.json | 44 +++++++++++++++++++++ tests/unit/test_delete_bid.py | 12 +++--- tests/unit/test_delete_question.py | 14 +++---- tests/unit/test_get_bid_by_id.py | 8 ++-- tests/unit/test_get_bids.py | 20 +++++----- tests/unit/test_get_question_by_id.py | 12 +++--- tests/unit/test_get_questions.py | 24 ++++++------ tests/unit/test_phase_schema.py | 4 +- tests/unit/test_post_bid.py | 8 ++-- tests/unit/test_post_question.py | 10 ++--- tests/unit/test_update_bid_by_id.py | 16 ++++---- tests/unit/test_update_bid_status.py | 14 +++---- tests/unit/test_update_question.py | 10 ++--- 16 files changed, 130 insertions(+), 132 deletions(-) create mode 100644 tests/integration/data.json diff --git a/Makefile b/Makefile index 1d00bee..645e178 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: help auth clean dbclean mongostart mongostop run setup swag test test-setup +.PHONY: help auth clean dbclean mongostart mongostop run setup swag test test-dbclean test-integration test-setup help: @echo "make help - display this help" @@ -20,6 +20,8 @@ help: @echo "make swag - open swagger documentation" @echo "make setup - setup the application database" @echo "make test - run tests and coverage report" + @echo "make test-dbclean - clear the test database" + @echo "make test-integration - run integration tests in test environment" @echo "make test-setup - setup the test database" @echo "make helptools - display help for tools" diff --git a/conftest.py b/conftest.py index cb406f0..bfd02aa 100644 --- a/conftest.py +++ b/conftest.py @@ -2,61 +2,18 @@ This file contains fixtures that are used by multiple tests. """ +import json import jwt import logging import os import pytest from app import create_app from dotenv import load_dotenv -from dbconfig.mongo_setup import get_db load_dotenv() - -test_data = [ - { - "_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": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", - "self": "/api/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": "a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7", - "alias": "ACME", - "bid_date": "2023-07-05", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", - "client": "ACME Corporation", - "failed": {"has_score": True, "out_of": 36, "phase": 2, "score": 15}, - "feedback": { - "description": "Feedback from client in detail", - "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback", - }, - "last_updated": "2023-08-01T14:30:20.123456", - "links": { - "questions": "/api/bids/a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7/questions", - "self": "/api/bids/a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7", - }, - "status": "in_progress", - "success": [{"has_score": True, "out_of": 36, "phase": 1, "score": 28}], - "tender": "Data Analytics Solution", - "was_successful": False, - }, -] +with open("./tests/integration/data.json") as data: + test_data = json.load(data) @pytest.fixture @@ -73,12 +30,14 @@ def integration_setup_and_teardown(test_app): def test_app(): os.environ["CONFIG_TYPE"] = "config.TestingConfig" app = create_app() - yield app + with app.app_context(): + yield app @pytest.fixture(scope="session") def test_client(test_app): - return test_app.test_client() + with test_app.app_context(): + return test_app.test_client() @pytest.fixture(autouse=True) diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index a5fa9a8..a694840 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -10,13 +10,6 @@ def get_db(DB_HOST, DB_PORT, DB_NAME): - # DB_HOST = os.getenv("DB_HOST") - # DB_PORT = 27017 - # DB_NAME = os.getenv("DB_NAME") - - # if os.environ.get("TESTING"): - # DB_NAME = os.getenv("TEST_DB_NAME") - # Create a new client and connect to the server client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) return client[DB_NAME] diff --git a/tests/integration/data.json b/tests/integration/data.json new file mode 100644 index 0000000..d09e6b2 --- /dev/null +++ b/tests/integration/data.json @@ -0,0 +1,44 @@ +[ + { + "_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": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + "self": "/api/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": "a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7", + "alias": "ACME", + "bid_date": "2023-07-05", + "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "client": "ACME Corporation", + "failed": {"has_score": true, "out_of": 36, "phase": 2, "score": 15}, + "feedback": { + "description": "Feedback from client in detail", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder/feedback" + }, + "last_updated": "2023-08-01T14:30:20.123456", + "links": { + "questions": "/api/bids/a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7/questions", + "self": "/api/bids/a5e8f31b-d848-4e87-b5c9-8db5a9d72bc7" + }, + "status": "in_progress", + "success": [{"has_score": true, "out_of": 36, "phase": 1, "score": 28}], + "tender": "Data Analytics Solution", + "was_successful": false + } +] \ No newline at end of file diff --git a/tests/unit/test_delete_bid.py b/tests/unit/test_delete_bid.py index ab66e8e..c6958c2 100644 --- a/tests/unit/test_delete_bid.py +++ b/tests/unit/test_delete_bid.py @@ -5,7 +5,7 @@ # Case 1: Successful delete a bid by changing status to deleted -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_delete_bid_success(mock_db, test_client, admin_jwt): mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", @@ -19,7 +19,7 @@ def test_delete_bid_success(mock_db, test_client, admin_jwt): # Case 2: Failed to call database -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_delete_bid_connection_error(mock_db, test_client, admin_jwt): mock_db["bids"].find_one_and_update.side_effect = Exception response = test_client.delete( @@ -31,7 +31,7 @@ def test_delete_bid_connection_error(mock_db, test_client, admin_jwt): # Case 3: Validation error -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_delete_bid_validation_error(mock_db, test_client, admin_jwt): response = test_client.delete( "/api/bids/invalid_bid_id", headers={"Authorization": f"Bearer {admin_jwt}"} @@ -41,7 +41,7 @@ def test_delete_bid_validation_error(mock_db, test_client, admin_jwt): # Case 4: Bid not found -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_delete_bid_not_found(mock_db, test_client, admin_jwt): mock_db["bids"].find_one_and_update.return_value = None @@ -56,7 +56,7 @@ def test_delete_bid_not_found(mock_db, test_client, admin_jwt): # Case 5: Unauthorized - invalid token -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_delete_bid_unauthorized(mock_db, test_client): mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", @@ -71,7 +71,7 @@ def test_delete_bid_unauthorized(mock_db, test_client): # Case 6: Forbidden - not admin -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_delete_bid_forbidden(mock_db, test_client, basic_jwt): mock_db["bids"].find_one_and_update.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", diff --git a/tests/unit/test_delete_question.py b/tests/unit/test_delete_question.py index 7f43135..55ecb54 100644 --- a/tests/unit/test_delete_question.py +++ b/tests/unit/test_delete_question.py @@ -5,7 +5,7 @@ # Case 1: Successful hard delete question -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_delete_question_success(mock_db, test_client, admin_jwt): mock_db["bids"].find_one_return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", @@ -35,7 +35,7 @@ def test_delete_question_success(mock_db, test_client, admin_jwt): # Case 2: Failed to call database -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_delete_question_connection_error(mock_db, test_client, admin_jwt): mock_db["questions"].delete_one.side_effect = Exception response = test_client.delete( @@ -47,7 +47,7 @@ def test_delete_question_connection_error(mock_db, test_client, admin_jwt): # Case 3: Validation error -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_delete_question_validation_error(mock_db, test_client, admin_jwt): response = test_client.delete( "/api/bids/invalid-bid-id/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", @@ -58,7 +58,7 @@ def test_delete_question_validation_error(mock_db, test_client, admin_jwt): # Case 4: Related bid not found -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_delete_question_bid_not_found(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.return_value = None response = test_client.delete( @@ -75,7 +75,7 @@ def test_delete_question_bid_not_found(mock_db, test_client, admin_jwt): # Case 5: Unauthorized - invalid token -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_delete_question_unauthorized(mock_db, test_client): response = test_client.delete( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", @@ -86,7 +86,7 @@ def test_delete_question_unauthorized(mock_db, test_client): # Case 6: Forbidden - not admin -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_delete_question_forbidden(mock_db, test_client, basic_jwt): response = test_client.delete( "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", @@ -97,7 +97,7 @@ def test_delete_question_forbidden(mock_db, test_client, basic_jwt): # # Case 7: Idempotence - question not found / already deleted -# @patch("api.controllers.question_controller.db") +# @patch("api.controllers.question_controller.current_app.db") # def test_delete_question_idempotence(mock_db, test_client, admin_jwt): # response = test_client.delete( # "/api/bids/1ff45b42-b72a-464c-bde9-9bead14a07b9/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e", diff --git a/tests/unit/test_get_bid_by_id.py b/tests/unit/test_get_bid_by_id.py index c97ce05..83a43d2 100644 --- a/tests/unit/test_get_bid_by_id.py +++ b/tests/unit/test_get_bid_by_id.py @@ -6,7 +6,7 @@ # Case 1: Successful get_bid_by_id -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bid_by_id_success(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = { "_id": "1ff45b42-b72a-464c-bde9-9bead14a07b9", @@ -49,7 +49,7 @@ def test_get_bid_by_id_success(mock_db, test_client, api_key): # Case 2: Connection error -@patch("api.controllers.bid_controller.db", side_effect=Exception) +@patch("api.controllers.bid_controller.current_app.db", side_effect=Exception) def test_get_bid_by_id_connection_error(mock_db, test_client, api_key): mock_db["bids"].find_one.side_effect = Exception response = test_client.get( @@ -61,7 +61,7 @@ def test_get_bid_by_id_connection_error(mock_db, test_client, api_key): # Case 3: Bid not found -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bid_by_id_not_found(mock_db, test_client, api_key): mock_db["bids"].find_one.return_value = None @@ -78,7 +78,7 @@ def test_get_bid_by_id_not_found(mock_db, test_client, api_key): # Case 4: Validation error -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bid_by_id_validation_error(mock_db, test_client, api_key): response = test_client.get( "/api/bids/invalid_bid_id", diff --git a/tests/unit/test_get_bids.py b/tests/unit/test_get_bids.py index 791d31a..76a85af 100644 --- a/tests/unit/test_get_bids.py +++ b/tests/unit/test_get_bids.py @@ -2,7 +2,7 @@ # Case 1: Successful get -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bids_success(mock_db, test_client, api_key, default_limit, default_offset): # Mock the find method of the db object sample_data = [ @@ -36,7 +36,7 @@ def test_get_bids_success(mock_db, test_client, api_key, default_limit, default_ # Case 2: Links prepended with hostname -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_links_with_host(mock_db, test_client, api_key): sample_data = [ { @@ -73,7 +73,7 @@ def test_links_with_host(mock_db, test_client, api_key): # Case 3: Connection error -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bids_connection_error(mock_db, test_client, api_key): mock_db["bids"].find.side_effect = Exception response = test_client.get( @@ -84,7 +84,7 @@ def test_get_bids_connection_error(mock_db, test_client, api_key): # Case 4: Unauthorized / invalid api key -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bids_unauthorized(mock_db, test_client): response = test_client.get( "/api/bids", headers={"host": "localhost:8080", "X-API-Key": "INVALID_API_KEY"} @@ -94,7 +94,7 @@ def test_get_bids_unauthorized(mock_db, test_client): # Case 5: Invalid offset - greater than maximum -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bids_max_offset(mock_db, test_client, api_key, max_offset): invalid_offset = int(max_offset) + 1 sample_data = [ @@ -129,7 +129,7 @@ def test_get_bids_max_offset(mock_db, test_client, api_key, max_offset): # Case 6: Invalid offset - not a number -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bids_nan_offset(mock_db, test_client, api_key, max_offset): invalid_offset = "five" sample_data = [ @@ -164,7 +164,7 @@ def test_get_bids_nan_offset(mock_db, test_client, api_key, max_offset): # Case 7: Invalid offset - negative number -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bids_negative_offset(mock_db, test_client, api_key, max_offset): invalid_offset = -1 sample_data = [ @@ -199,7 +199,7 @@ def test_get_bids_negative_offset(mock_db, test_client, api_key, max_offset): # Case 8: Invalid limit - greater than maximum -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bids_max_limit(mock_db, test_client, api_key, max_limit): invalid_limit = int(max_limit) + 1 sample_data = [ @@ -234,7 +234,7 @@ def test_get_bids_max_limit(mock_db, test_client, api_key, max_limit): # Case 9: Invalid limit - not a number -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bids_nan_limit(mock_db, test_client, api_key, max_limit): invalid_limit = "five" sample_data = [ @@ -269,7 +269,7 @@ def test_get_bids_nan_limit(mock_db, test_client, api_key, max_limit): # Case 10: Invalid limit - negative number -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_get_bids_negative_limit(mock_db, test_client, api_key, max_limit): invalid_limit = -1 sample_data = [ diff --git a/tests/unit/test_get_question_by_id.py b/tests/unit/test_get_question_by_id.py index 8bd3474..7f4bb8e 100644 --- a/tests/unit/test_get_question_by_id.py +++ b/tests/unit/test_get_question_by_id.py @@ -5,7 +5,7 @@ # Case 1: Successful get -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_single_question_success(mock_db, test_client, api_key): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -48,7 +48,7 @@ def test_get_single_question_success(mock_db, test_client, api_key): # Case 2: Links prepended with hostname -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_single_question_links_with_host(mock_db, test_client, api_key): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -97,7 +97,7 @@ def test_single_question_links_with_host(mock_db, test_client, api_key): # Case 3: Connection error -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_single_question_connection_error(mock_db, test_client, api_key): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -119,7 +119,7 @@ def test_get_single_question_connection_error(mock_db, test_client, api_key): # Case 4: Unauthorized / invalid api key -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_single_question_unauthorized(mock_db, test_client): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -152,7 +152,7 @@ def test_get_single_question_unauthorized(mock_db, test_client): # Case 5: No question found for the given ID -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_no_question_found_by_id(mock_db, test_client, api_key): # Set up the sample question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -174,7 +174,7 @@ def test_no_question_found_by_id(mock_db, test_client, api_key): # Case 6: Validation error -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_question_by_id_validation_error(mock_db, test_client, api_key): # Set up the sample question ID sample_bid_id = "Invalid bid Id" diff --git a/tests/unit/test_get_questions.py b/tests/unit/test_get_questions.py index 19ffdca..c5e4301 100644 --- a/tests/unit/test_get_questions.py +++ b/tests/unit/test_get_questions.py @@ -6,7 +6,7 @@ # Case 1: Successful get -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_success( mock_db, test_client, api_key, default_limit, default_offset ): @@ -54,7 +54,7 @@ def test_get_questions_success( # Case 2: Links prepended with hostname -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_links_with_host(mock_db, test_client, api_key): # Set up the sample data and expected result sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -105,7 +105,7 @@ def test_links_with_host(mock_db, test_client, api_key): # Case 3: Connection error -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_connection_error(mock_db, test_client, api_key): # Set up the sample bid ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -126,7 +126,7 @@ def test_get_questions_connection_error(mock_db, test_client, api_key): # Case 4: Unauthorized / invalid api key -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_unauthorized(mock_db, test_client): # Set up the sample bid ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -157,7 +157,7 @@ def test_get_questions_unauthorized(mock_db, test_client): # Case 5: No questions found -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_no_questions_found(mock_db, test_client, api_key): # Set up the sample bid ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -178,7 +178,7 @@ def test_no_questions_found(mock_db, test_client, api_key): # Case 6: Validation error -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_bid_id_validation_error(mock_db, test_client, api_key): # Set up the sample question ID sample_bid_id = "Invalid bid Id" @@ -192,7 +192,7 @@ def test_get_questions_bid_id_validation_error(mock_db, test_client, api_key): # Case 7: Invalid offset - greater than maximum -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_max_offset(mock_db, test_client, api_key, max_offset): invalid_offset = int(max_offset) + 1 sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -236,7 +236,7 @@ def test_get_questions_max_offset(mock_db, test_client, api_key, max_offset): # Case 8: Invalid offset - not a number -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_nan_offset(mock_db, test_client, api_key, max_offset): invalid_offset = "five" sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -280,7 +280,7 @@ def test_get_questions_nan_offset(mock_db, test_client, api_key, max_offset): # Case 9: Invalid offset - negative number -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_negative_offset(mock_db, test_client, api_key, max_offset): invalid_offset = -1 sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -324,7 +324,7 @@ def test_get_questions_negative_offset(mock_db, test_client, api_key, max_offset # Case 10: Invalid limit - greater than maximum -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_max_limit(mock_db, test_client, api_key, max_limit): invalid_limit = int(max_limit) + 1 sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -368,7 +368,7 @@ def test_get_questions_max_limit(mock_db, test_client, api_key, max_limit): # Case 11: Invalid limit - not a number -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_nan_limit(mock_db, test_client, api_key, max_limit): invalid_limit = "ten" sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -412,7 +412,7 @@ def test_get_questions_nan_limit(mock_db, test_client, api_key, max_limit): # Case 12: Invalid limit - negative number -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_get_questions_negative_limit(mock_db, test_client, api_key, max_limit): invalid_limit = -1 sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" diff --git a/tests/unit/test_phase_schema.py b/tests/unit/test_phase_schema.py index 468882b..5d756e3 100644 --- a/tests/unit/test_phase_schema.py +++ b/tests/unit/test_phase_schema.py @@ -5,7 +5,7 @@ # Case 1: score is mandatory when has_score is set to True -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_score_is_mandatory(mock_db, test_client, basic_jwt): data = { "tender": "Business Intelligence and Data Warehousing", @@ -25,7 +25,7 @@ def test_score_is_mandatory(mock_db, test_client, basic_jwt): # Case 2: out_of is mandatory when has_score is set to True -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_out_of_is_mandatory(mock_db, test_client, basic_jwt): data = { "tender": "Business Intelligence and Data Warehousing", diff --git a/tests/unit/test_post_bid.py b/tests/unit/test_post_bid.py index 225dd3d..56231d1 100644 --- a/tests/unit/test_post_bid.py +++ b/tests/unit/test_post_bid.py @@ -5,7 +5,7 @@ # Case 1: Successful post -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_post_is_successful(mock_db, test_client, basic_jwt): data = { "tender": "Business Intelligence and Data Warehousing", @@ -49,7 +49,7 @@ def test_post_is_successful(mock_db, test_client, basic_jwt): # Case 2: Missing mandatory fields -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_field_missing(mock_db, test_client, basic_jwt): data = {"client": "Sample Client", "bid_date": "2023-06-20"} @@ -63,7 +63,7 @@ def test_field_missing(mock_db, test_client, basic_jwt): # Case 3: Connection error -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_post_bid_connection_error(mock_db, test_client, basic_jwt): data = { "tender": "Business Intelligence and Data Warehousing", @@ -89,7 +89,7 @@ def test_post_bid_connection_error(mock_db, test_client, basic_jwt): # Case 4: Unauthorized - invalid token -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_post_bid_unauthorized(mock_db, test_client): data = { "tender": "Business Intelligence and Data Warehousing", diff --git a/tests/unit/test_post_question.py b/tests/unit/test_post_question.py index 3707b1a..336c493 100644 --- a/tests/unit/test_post_question.py +++ b/tests/unit/test_post_question.py @@ -5,7 +5,7 @@ # Case 1: Successful post -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_post_is_successful(mock_db, test_client, basic_jwt): data = { "description": "This is a question", @@ -46,7 +46,7 @@ def test_post_is_successful(mock_db, test_client, basic_jwt): # Case 2: Missing mandatory fields -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_post_question_field_missing(mock_db, test_client, basic_jwt): data = { "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", @@ -68,7 +68,7 @@ def test_post_question_field_missing(mock_db, test_client, basic_jwt): # Case 3: Connection error -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_post_question_connection_error(mock_db, test_client, basic_jwt): data = { "description": "This is a question", @@ -91,7 +91,7 @@ def test_post_question_connection_error(mock_db, test_client, basic_jwt): # Case 4: Unauthorized - invalid token -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_post_question_unauthorized(mock_db, test_client): data = { "description": "This is a question", @@ -114,7 +114,7 @@ def test_post_question_unauthorized(mock_db, test_client): # Case 5: Related bid not found -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_post_question_bid_not_found(mock_db, test_client, basic_jwt): data = { "description": "This is a question", diff --git a/tests/unit/test_update_bid_by_id.py b/tests/unit/test_update_bid_by_id.py index d62d768..6f9d271 100644 --- a/tests/unit/test_update_bid_by_id.py +++ b/tests/unit/test_update_bid_by_id.py @@ -5,7 +5,7 @@ # Case 1: Successful update -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_update_bid_by_id_success(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", @@ -33,7 +33,7 @@ def test_update_bid_by_id_success(mock_db, test_client, basic_jwt): # Case 2: Invalid user input -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_input_validation(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", @@ -65,7 +65,7 @@ def test_input_validation(mock_db, test_client, basic_jwt): # Case 3: Bid not found -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_bid_not_found(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" @@ -80,7 +80,7 @@ def test_bid_not_found(mock_db, test_client, basic_jwt): # Case 4: Cannot update status -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_cannot_update_status(mock_db, test_client, basic_jwt): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {"status": "deleted"} @@ -94,7 +94,7 @@ def test_cannot_update_status(mock_db, test_client, basic_jwt): # Case 5: Failed to call database -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_update_by_id_find_error(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", @@ -118,7 +118,7 @@ def test_update_by_id_find_error(mock_db, test_client, basic_jwt): # Case 6: Update failed field -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_update_failed(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", @@ -153,7 +153,7 @@ def test_update_failed(mock_db, test_client, basic_jwt): # Case 7: Update success field -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_update_success(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", @@ -193,7 +193,7 @@ def test_update_success(mock_db, test_client, basic_jwt): # Case 8: Unauthorized - invalid token -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_update_bid_by_id_unauthorized(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "9f688442-b535-4683-ae1a-a64c1a3b8616", diff --git a/tests/unit/test_update_bid_status.py b/tests/unit/test_update_bid_status.py index 2608897..56d6d3e 100644 --- a/tests/unit/test_update_bid_status.py +++ b/tests/unit/test_update_bid_status.py @@ -5,7 +5,7 @@ # Case 1: Successful update -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_update_bid_status_success(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", @@ -40,7 +40,7 @@ def test_update_bid_status_success(mock_db, test_client, admin_jwt): # Case 2: Invalid status -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_invalid_status(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", @@ -75,7 +75,7 @@ def test_invalid_status(mock_db, test_client, admin_jwt): # Case 3: Empty request body -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_empty_request(mock_db, test_client, admin_jwt): bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" update = {} @@ -89,7 +89,7 @@ def test_empty_request(mock_db, test_client, admin_jwt): # Case 4: Bid not found -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_bid_not_found(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.return_value = None bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" @@ -104,7 +104,7 @@ def test_bid_not_found(mock_db, test_client, admin_jwt): # Case 5: Failed to call database -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_update_status_find_error(mock_db, test_client, admin_jwt): mock_db["bids"].find_one.side_effect = Exception bid_id = "9f688442-b535-4683-ae1a-a64c1a3b8616" @@ -119,7 +119,7 @@ def test_update_status_find_error(mock_db, test_client, admin_jwt): # Case 6: Unauthorized - invalid token -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_update_bid_status_unauthorized(mock_db, test_client): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", @@ -152,7 +152,7 @@ def test_update_bid_status_unauthorized(mock_db, test_client): # Case 7: Forbidden - not admin -@patch("api.controllers.bid_controller.db") +@patch("api.controllers.bid_controller.current_app.db") def test_update_bid_status_forbidden(mock_db, test_client, basic_jwt): mock_db["bids"].find_one.return_value = { "_id": "4141fac8-8879-4169-a46d-2effb1f515f6", diff --git a/tests/unit/test_update_question.py b/tests/unit/test_update_question.py index 3e4ac4e..9e56402 100644 --- a/tests/unit/test_update_question.py +++ b/tests/unit/test_update_question.py @@ -5,7 +5,7 @@ # Case 1: Successful question update -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_update_question_success(mock_db, test_client, basic_jwt): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -54,7 +54,7 @@ def test_update_question_success(mock_db, test_client, basic_jwt): # Case 2: Invalid user input -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_update_question_invalid_input(mock_db, test_client, basic_jwt): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -93,7 +93,7 @@ def test_update_question_invalid_input(mock_db, test_client, basic_jwt): # Case 3: Question not found -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_question_not_found(mock_db, test_client, basic_jwt): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -114,7 +114,7 @@ def test_question_not_found(mock_db, test_client, basic_jwt): # Case 4: Exception handling - Internal Server Error -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_exception_internal_server_error(mock_db, test_client, basic_jwt): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" @@ -133,7 +133,7 @@ def test_exception_internal_server_error(mock_db, test_client, basic_jwt): # Case 5: Empty request body -@patch("api.controllers.question_controller.db") +@patch("api.controllers.question_controller.current_app.db") def test_update_question_invalid_empty_request_body(mock_db, test_client, basic_jwt): # Set up the sample bid ID and question ID sample_bid_id = "66fb5dba-f129-413a-b12e-5a68b5a647d6" From 611ef19b0ce731d9751e353f34b416ed0f932236 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 15 Sep 2023 15:57:16 +0100 Subject: [PATCH 197/208] refactor: renamed integ test file; refactored conftest; removed print statement from controller --- api/controllers/bid_controller.py | 1 - conftest.py | 25 ++++++----- tests/integration/post_and_delete_admin.py | 44 ------------------- tests/integration/post_bid_jwt.py | 36 --------------- ...t_bids_with_apikey.py => test_get_bids.py} | 6 +-- 5 files changed, 15 insertions(+), 97 deletions(-) delete mode 100644 tests/integration/post_and_delete_admin.py delete mode 100644 tests/integration/post_bid_jwt.py rename tests/integration/{test_get_bids_with_apikey.py => test_get_bids.py} (75%) diff --git a/api/controllers/bid_controller.py b/api/controllers/bid_controller.py index 28208b4..86140ed 100644 --- a/api/controllers/bid_controller.py +++ b/api/controllers/bid_controller.py @@ -32,7 +32,6 @@ @require_api_key def get_bids(): try: - # print(current_app.db) db = current_app.db logger.info(f"Handling request {g.request_id}") hostname = request.headers.get("host") diff --git a/conftest.py b/conftest.py index bfd02aa..66b8a75 100644 --- a/conftest.py +++ b/conftest.py @@ -16,16 +16,6 @@ test_data = json.load(data) -@pytest.fixture -def integration_setup_and_teardown(test_app): - db = test_app.db - collection = db["bids"] - collection.insert_many(test_data) - yield - collection.delete_many({}) - # print("AFTER") - - @pytest.fixture(scope="session") def test_app(): os.environ["CONFIG_TYPE"] = "config.TestingConfig" @@ -40,13 +30,24 @@ def test_client(test_app): return test_app.test_client() +@pytest.fixture +def integration_setup_and_teardown(test_app): + db = test_app.db + collection = db["bids"] + collection.insert_many(test_data) + print("----------Test database populated----------") + yield + collection.delete_many({}) + print("----------Test database cleared----------") + + @pytest.fixture(autouse=True) def pause_logging(): logging.disable(logging.CRITICAL) - print("----------Logging disabled----------") + # print("----------Logging disabled----------") yield logging.disable(logging.NOTSET) - print("----------Logging re-enabled----------") + # print("----------Logging re-enabled----------") @pytest.fixture(scope="session") diff --git a/tests/integration/post_and_delete_admin.py b/tests/integration/post_and_delete_admin.py deleted file mode 100644 index 881f0a7..0000000 --- a/tests/integration/post_and_delete_admin.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -This script is used to test the post and delete functionality of the API. -""" - -import json -import requests - - -user_info = {"username": "Pira"} - -response = requests.post( - "http://localhost:5000/authorise/", - data=json.dumps(user_info), - headers={"Content-Type": "application/json"}, -) - -token = response.json()["jwt"] - -# print(token) - -data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-06-23", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -} - -fstring = f"Bearer {token}" - -headers = {"Content-Type": "application/json", "Authorization": fstring} - -post_response = requests.post( - "http://localhost:8080/api/bids", data=json.dumps(data), headers=headers -) - -# print(post_response) - -bid_id = post_response.json()["_id"] - -delete_url = f"http://localhost:8080/api/bids/{bid_id}" - -delete_response = requests.delete(delete_url, headers=headers) - -print(delete_response) diff --git a/tests/integration/post_bid_jwt.py b/tests/integration/post_bid_jwt.py deleted file mode 100644 index 610412c..0000000 --- a/tests/integration/post_bid_jwt.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -This script is used to test the API endpoint for posting a bid. -""" - -import json -import requests - - -user_info = {"username": "Tester McTestface"} - -response = requests.post( - "http://localhost:5000/authorise/", - data=json.dumps(user_info), - headers={"Content-Type": "application/json"}, -) - -token = response.json()["jwt"] - -print(token) - -data = { - "tender": "Business Intelligence and Data Warehousing", - "client": "Office for National Statistics", - "bid_date": "2023-06-23", - "bid_folder_url": "https://organisation.sharepoint.com/Docs/dummyfolder", -} - -fstring = f"Bearer {token}" - -headers = {"Content-Type": "application/json", "Authorization": fstring} - -post_response = requests.post( - "http://localhost:8080/api/bids", data=json.dumps(data), headers=headers -) - -print(post_response) diff --git a/tests/integration/test_get_bids_with_apikey.py b/tests/integration/test_get_bids.py similarity index 75% rename from tests/integration/test_get_bids_with_apikey.py rename to tests/integration/test_get_bids.py index b7cfd2a..ad156c4 100644 --- a/tests/integration/test_get_bids_with_apikey.py +++ b/tests/integration/test_get_bids.py @@ -1,12 +1,10 @@ -import pytest, os -import requests +import pytest pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") -# @pytest.mark.integration -def test_get_bids_with_api_key(test_client, api_key): +def test_get_bids(test_client, api_key): headers = { "host": "localhost:8080", "Content-Type": "application/json", From b494dc0116ea8ecf59f3a947a997ea8a7bce1de1 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 15 Sep 2023 16:01:51 +0100 Subject: [PATCH 198/208] refactor: removed pytest.ini file --- pytest.ini | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index bed64ef..0000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -markers = - integration \ No newline at end of file From d72e8698ba00f590466969e946f33bcd408a62b3 Mon Sep 17 00:00:00 2001 From: piratejas Date: Fri, 15 Sep 2023 17:31:29 +0100 Subject: [PATCH 199/208] test: added integ tests for bid endpoints --- tests/integration/test_delete_bid.py | 14 ++++++++++++++ tests/integration/test_get_bid_by_id.py | 17 +++++++++++++++++ tests/integration/test_get_bids.py | 1 - tests/integration/test_post_bid.py | 23 +++++++++++++++++++++++ tests/integration/test_update_bid.py | 20 ++++++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/integration/test_delete_bid.py create mode 100644 tests/integration/test_get_bid_by_id.py create mode 100644 tests/integration/test_post_bid.py create mode 100644 tests/integration/test_update_bid.py diff --git a/tests/integration/test_delete_bid.py b/tests/integration/test_delete_bid.py new file mode 100644 index 0000000..7484cbb --- /dev/null +++ b/tests/integration/test_delete_bid.py @@ -0,0 +1,14 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") + + +def test_delete_bid(test_app, test_client, admin_jwt): + headers = {"host": "localhost:8080", "Authorization": f"Bearer {admin_jwt}"} + id = "be15c306-c85b-4e67-a9f6-682553c065a1" + + response = test_client.delete(f"/api/bids/{id}", headers=headers) + + assert response.status_code == 204 + assert test_app.db["bids"].count_documents({"status": {"$ne": "deleted"}}) == 1 diff --git a/tests/integration/test_get_bid_by_id.py b/tests/integration/test_get_bid_by_id.py new file mode 100644 index 0000000..8abfe5b --- /dev/null +++ b/tests/integration/test_get_bid_by_id.py @@ -0,0 +1,17 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") + + +def test_get_bid_by_id(test_client, api_key): + headers = { + "host": "localhost:8080", + "X-API-Key": api_key, + } + id = "be15c306-c85b-4e67-a9f6-682553c065a1" + + response = test_client.get(f"/api/bids/{id}", headers=headers) + + assert response.status_code == 200 + assert response.get_json()["_id"] == id diff --git a/tests/integration/test_get_bids.py b/tests/integration/test_get_bids.py index ad156c4..cb56064 100644 --- a/tests/integration/test_get_bids.py +++ b/tests/integration/test_get_bids.py @@ -7,7 +7,6 @@ def test_get_bids(test_client, api_key): headers = { "host": "localhost:8080", - "Content-Type": "application/json", "X-API-Key": api_key, } diff --git a/tests/integration/test_post_bid.py b/tests/integration/test_post_bid.py new file mode 100644 index 0000000..4c0e251 --- /dev/null +++ b/tests/integration/test_post_bid.py @@ -0,0 +1,23 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") + + +def test_get_bid_by_id(test_app, test_client, basic_jwt): + headers = { + "host": "localhost:8080", + "Content-Type": "application/json", + "Authorization": f"Bearer {basic_jwt}", + } + + data = { + "bid_date": "2023-07-10", + "client": "XYZ Innovations", + "tender": "Advanced Analytics Platform", + } + + response = test_client.post(f"/api/bids", json=data, headers=headers) + + assert response.status_code == 201 + assert test_app.db["bids"].count_documents({}) == 3 diff --git a/tests/integration/test_update_bid.py b/tests/integration/test_update_bid.py new file mode 100644 index 0000000..7c8314f --- /dev/null +++ b/tests/integration/test_update_bid.py @@ -0,0 +1,20 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") + + +def test_delete_bid(test_app, test_client, basic_jwt): + headers = {"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"} + + id = "be15c306-c85b-4e67-a9f6-682553c065a1" + + data = {"tender": "THIS IS UPDATED"} + + response = test_client.put(f"/api/bids/{id}", json=data, headers=headers) + + bid = test_app.db["bids"].find_one({"_id": id}) + + assert response.status_code == 200 + assert response.get_json()["tender"] == "THIS IS UPDATED" + assert bid["tender"] == "THIS IS UPDATED" From 8da616b8a22bec6963aa68ea1fbb50ac99581ef2 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 18 Sep 2023 10:03:15 +0100 Subject: [PATCH 200/208] test: renamed test_data to bids_data in json and conftest --- conftest.py | 6 +++--- tests/integration/{data.json => bids.json} | 0 tests/integration/test_post_bid.py | 2 +- tests/integration/test_update_bid.py | 2 +- tests/integration/test_update_bid_status.py | 20 ++++++++++++++++++++ 5 files changed, 25 insertions(+), 5 deletions(-) rename tests/integration/{data.json => bids.json} (100%) create mode 100644 tests/integration/test_update_bid_status.py diff --git a/conftest.py b/conftest.py index 66b8a75..00ed631 100644 --- a/conftest.py +++ b/conftest.py @@ -12,8 +12,8 @@ load_dotenv() -with open("./tests/integration/data.json") as data: - test_data = json.load(data) +with open("./tests/integration/bids.json") as bids: + bids_data = json.load(bids) @pytest.fixture(scope="session") @@ -34,7 +34,7 @@ def test_client(test_app): def integration_setup_and_teardown(test_app): db = test_app.db collection = db["bids"] - collection.insert_many(test_data) + collection.insert_many(bids_data) print("----------Test database populated----------") yield collection.delete_many({}) diff --git a/tests/integration/data.json b/tests/integration/bids.json similarity index 100% rename from tests/integration/data.json rename to tests/integration/bids.json diff --git a/tests/integration/test_post_bid.py b/tests/integration/test_post_bid.py index 4c0e251..a34da49 100644 --- a/tests/integration/test_post_bid.py +++ b/tests/integration/test_post_bid.py @@ -4,7 +4,7 @@ pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") -def test_get_bid_by_id(test_app, test_client, basic_jwt): +def test_post_bid(test_app, test_client, basic_jwt): headers = { "host": "localhost:8080", "Content-Type": "application/json", diff --git a/tests/integration/test_update_bid.py b/tests/integration/test_update_bid.py index 7c8314f..646fe1c 100644 --- a/tests/integration/test_update_bid.py +++ b/tests/integration/test_update_bid.py @@ -4,7 +4,7 @@ pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") -def test_delete_bid(test_app, test_client, basic_jwt): +def test_update_bid(test_app, test_client, basic_jwt): headers = {"host": "localhost:8080", "Authorization": f"Bearer {basic_jwt}"} id = "be15c306-c85b-4e67-a9f6-682553c065a1" diff --git a/tests/integration/test_update_bid_status.py b/tests/integration/test_update_bid_status.py new file mode 100644 index 0000000..833c9ae --- /dev/null +++ b/tests/integration/test_update_bid_status.py @@ -0,0 +1,20 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") + + +def test_update_bid_status(test_app, test_client, admin_jwt): + headers = {"host": "localhost:8080", "Authorization": f"Bearer {admin_jwt}"} + + id = "be15c306-c85b-4e67-a9f6-682553c065a1" + + data = {"status": "completed"} + + response = test_client.put(f"/api/bids/{id}/status", json=data, headers=headers) + + bid = test_app.db["bids"].find_one({"_id": id}) + + assert response.status_code == 200 + assert response.get_json()["status"] == "completed" + assert bid["status"] == "completed" From 588b193b851f1b0c49ffe1c48f4924e662f64256 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 18 Sep 2023 11:18:06 +0100 Subject: [PATCH 201/208] test: added exception for db connection failure --- conftest.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/conftest.py b/conftest.py index 00ed631..22647cb 100644 --- a/conftest.py +++ b/conftest.py @@ -34,11 +34,20 @@ def test_client(test_app): def integration_setup_and_teardown(test_app): db = test_app.db collection = db["bids"] - collection.insert_many(bids_data) - print("----------Test database populated----------") + try: + collection.insert_many(bids_data) + print("----------Test database populated----------") + except Exception as e: + print(f"Error while populating the test database: {str(e)}") + return + yield - collection.delete_many({}) - print("----------Test database cleared----------") + + try: + collection.delete_many({}) + print("----------Test database cleared----------") + except Exception as e: + print(f"Error while clearing the test database: {str(e)}") @pytest.fixture(autouse=True) From 353958e25d4e77a0ea9f3f11fde1510b8c4afa8a Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 18 Sep 2023 11:53:19 +0100 Subject: [PATCH 202/208] test: added questions collection setup fixture; changed error messages; created get questions integ test --- conftest.py | 33 ++++++++++++++--- tests/integration/questions.json | 40 +++++++++++++++++++++ tests/integration/test_delete_bid.py | 2 +- tests/integration/test_get_bid_by_id.py | 2 +- tests/integration/test_get_bids.py | 2 +- tests/integration/test_get_questions.py | 20 +++++++++++ tests/integration/test_post_bid.py | 2 +- tests/integration/test_update_bid.py | 2 +- tests/integration/test_update_bid_status.py | 2 +- 9 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 tests/integration/questions.json create mode 100644 tests/integration/test_get_questions.py diff --git a/conftest.py b/conftest.py index 22647cb..cb1002c 100644 --- a/conftest.py +++ b/conftest.py @@ -15,6 +15,9 @@ with open("./tests/integration/bids.json") as bids: bids_data = json.load(bids) +with open("./tests/integration/questions.json") as questions: + questions_data = json.load(questions) + @pytest.fixture(scope="session") def test_app(): @@ -31,23 +34,43 @@ def test_client(test_app): @pytest.fixture -def integration_setup_and_teardown(test_app): +def bids_db_setup_and_teardown(test_app): db = test_app.db collection = db["bids"] try: collection.insert_many(bids_data) - print("----------Test database populated----------") + print("----------Bids collection populated----------") + except Exception as e: + print(f"Error while populating the Bids collection: {str(e)}") + return + + yield + + try: + collection.delete_many({}) + print("----------Bids collection cleared----------") + except Exception as e: + print(f"Error while clearing the Bids collection: {str(e)}") + + +@pytest.fixture +def questions_db_setup_and_teardown(test_app): + db = test_app.db + collection = db["questions"] + try: + collection.insert_many(questions_data) + print("----------Questions collection populated----------") except Exception as e: - print(f"Error while populating the test database: {str(e)}") + print(f"Error while populating the Questions collection: {str(e)}") return yield try: collection.delete_many({}) - print("----------Test database cleared----------") + print("----------Questions collection cleared----------") except Exception as e: - print(f"Error while clearing the test database: {str(e)}") + print(f"Error while clearing the Questions collection: {str(e)}") @pytest.fixture(autouse=True) diff --git a/tests/integration/questions.json b/tests/integration/questions.json new file mode 100644 index 0000000..f8f4fd0 --- /dev/null +++ b/tests/integration/questions.json @@ -0,0 +1,40 @@ +[ + { + "_id": "6e7d3f8a-fab3-4ebf-8348-96d0808d325e", + "description": "This is a question", + "feedback": { + "description": "Good feedback", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder" + }, + "last_updated": "2023-08-01T15:08:33.187216", + "links": { + "bid": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1", + "self": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions/6e7d3f8a-fab3-4ebf-8348-96d0808d325e" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "respondents": ["JohnDoe", "JaneSmith"], + "response": "This is a sample response.", + "score": 3, + "status": "in_progress" + }, + { + "_id": "c9f6d25b-7d36-4cf3-89e7-1df2f1ca4e21", + "description": "Another question", + "feedback": { + "description": "Excellent feedback", + "url": "https://organisation.sharepoint.com/Docs/anotherfolder" + }, + "last_updated": "2023-08-01T10:20:15.123456", + "links": { + "bid": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1", + "self": "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions/c9f6d25b-7d36-4cf3-89e7-1df2f1ca4e21" + }, + "out_of": 3, + "question_url": "https://organisation.sharepoint.com/Docs/anotherfolder", + "respondents": ["JohnDoe", "JaneSmith", "AlexBrown"], + "response": "This is another response.", + "score": 2, + "status": "in_progress" + } +] \ No newline at end of file diff --git a/tests/integration/test_delete_bid.py b/tests/integration/test_delete_bid.py index 7484cbb..5048dc0 100644 --- a/tests/integration/test_delete_bid.py +++ b/tests/integration/test_delete_bid.py @@ -1,7 +1,7 @@ import pytest -pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") +pytestmark = pytest.mark.usefixtures("bids_db_setup_and_teardown") def test_delete_bid(test_app, test_client, admin_jwt): diff --git a/tests/integration/test_get_bid_by_id.py b/tests/integration/test_get_bid_by_id.py index 8abfe5b..072f72b 100644 --- a/tests/integration/test_get_bid_by_id.py +++ b/tests/integration/test_get_bid_by_id.py @@ -1,7 +1,7 @@ import pytest -pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") +pytestmark = pytest.mark.usefixtures("bids_db_setup_and_teardown") def test_get_bid_by_id(test_client, api_key): diff --git a/tests/integration/test_get_bids.py b/tests/integration/test_get_bids.py index cb56064..81d2f6e 100644 --- a/tests/integration/test_get_bids.py +++ b/tests/integration/test_get_bids.py @@ -1,7 +1,7 @@ import pytest -pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") +pytestmark = pytest.mark.usefixtures("bids_db_setup_and_teardown") def test_get_bids(test_client, api_key): diff --git a/tests/integration/test_get_questions.py b/tests/integration/test_get_questions.py new file mode 100644 index 0000000..e309d0e --- /dev/null +++ b/tests/integration/test_get_questions.py @@ -0,0 +1,20 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures( + "bids_db_setup_and_teardown", "questions_db_setup_and_teardown" +) + + +def test_get_questions(test_client, api_key): + headers = { + "host": "localhost:8080", + "X-API-Key": api_key, + } + + response = test_client.get( + "/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", headers=headers + ) + + assert response.status_code == 200 + assert len(response.get_json()["items"]) == 2 diff --git a/tests/integration/test_post_bid.py b/tests/integration/test_post_bid.py index a34da49..40de7f0 100644 --- a/tests/integration/test_post_bid.py +++ b/tests/integration/test_post_bid.py @@ -1,7 +1,7 @@ import pytest -pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") +pytestmark = pytest.mark.usefixtures("bids_db_setup_and_teardown") def test_post_bid(test_app, test_client, basic_jwt): diff --git a/tests/integration/test_update_bid.py b/tests/integration/test_update_bid.py index 646fe1c..630b79a 100644 --- a/tests/integration/test_update_bid.py +++ b/tests/integration/test_update_bid.py @@ -1,7 +1,7 @@ import pytest -pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") +pytestmark = pytest.mark.usefixtures("bids_db_setup_and_teardown") def test_update_bid(test_app, test_client, basic_jwt): diff --git a/tests/integration/test_update_bid_status.py b/tests/integration/test_update_bid_status.py index 833c9ae..9bb37f3 100644 --- a/tests/integration/test_update_bid_status.py +++ b/tests/integration/test_update_bid_status.py @@ -1,7 +1,7 @@ import pytest -pytestmark = pytest.mark.usefixtures("integration_setup_and_teardown") +pytestmark = pytest.mark.usefixtures("bids_db_setup_and_teardown") def test_update_bid_status(test_app, test_client, admin_jwt): From e9130d790dbcd8d285f4906173fc4679fc0121b9 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 18 Sep 2023 12:27:41 +0100 Subject: [PATCH 203/208] test: created tests for question endpoints --- conftest.py | 26 +++++++++------ request_examples/post_question.http | 4 +-- tests/integration/test_delete_question.py | 23 +++++++++++++ tests/integration/test_get_question_by_id.py | 22 ++++++++++++ tests/integration/test_post_bid.py | 3 ++ tests/integration/test_post_question.py | 35 ++++++++++++++++++++ tests/integration/test_update_question.py | 30 +++++++++++++++++ 7 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 tests/integration/test_delete_question.py create mode 100644 tests/integration/test_get_question_by_id.py create mode 100644 tests/integration/test_post_question.py create mode 100644 tests/integration/test_update_question.py diff --git a/conftest.py b/conftest.py index cb1002c..ec26ed8 100644 --- a/conftest.py +++ b/conftest.py @@ -39,18 +39,21 @@ def bids_db_setup_and_teardown(test_app): collection = db["bids"] try: collection.insert_many(bids_data) - print("----------Bids collection populated----------") + # print("----------Bids collection populated----------") except Exception as e: - print(f"Error while populating the Bids collection: {str(e)}") - return + raise ConnectionRefusedError( + f"Error while populating the Bids collection: {str(e)}" + ) yield try: collection.delete_many({}) - print("----------Bids collection cleared----------") + # print("----------Bids collection cleared----------") except Exception as e: - print(f"Error while clearing the Bids collection: {str(e)}") + raise ConnectionRefusedError( + f"Error while clearing the Bids collection: {str(e)}" + ) @pytest.fixture @@ -59,18 +62,21 @@ def questions_db_setup_and_teardown(test_app): collection = db["questions"] try: collection.insert_many(questions_data) - print("----------Questions collection populated----------") + # print("----------Questions collection populated----------") except Exception as e: - print(f"Error while populating the Questions collection: {str(e)}") - return + raise ConnectionRefusedError( + f"Error while populating the Questions collection: {str(e)}" + ) yield try: collection.delete_many({}) - print("----------Questions collection cleared----------") + # print("----------Questions collection cleared----------") except Exception as e: - print(f"Error while clearing the Questions collection: {str(e)}") + raise ConnectionRefusedError( + f"Error while clearing the Questions collection: {str(e)}" + ) @pytest.fixture(autouse=True) diff --git a/request_examples/post_question.http b/request_examples/post_question.http index a34cc55..512bb05 100644 --- a/request_examples/post_question.http +++ b/request_examples/post_question.http @@ -1,6 +1,6 @@ -PUT http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions/3eabf8e2-ed72-4e72-8798-2eb8ead0c1c6 HTTP/1.1 +POST http://localhost:8080/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikp1bGlvIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5MTU5MTg3M30.4Kq1IS-kOltqdIAUdQPX_m884kqSmzUnb6Ha6gEgz20 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlBpcmEiLCJhZG1pbiI6dHJ1ZSwiZXhwIjoxNjk1NjM5NzU5fQ.btdgSJbIC0rxeRI0CE5_mx-VvYbJKqey2ud0_mjoKoQ { "description": "This is a question", diff --git a/tests/integration/test_delete_question.py b/tests/integration/test_delete_question.py new file mode 100644 index 0000000..12f8529 --- /dev/null +++ b/tests/integration/test_delete_question.py @@ -0,0 +1,23 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures( + "bids_db_setup_and_teardown", "questions_db_setup_and_teardown" +) + + +def test_delete_question(test_app, test_client, admin_jwt): + headers = { + "host": "localhost:8080", + "Content-Type": "application/json", + "Authorization": f"Bearer {admin_jwt}", + } + id = "6e7d3f8a-fab3-4ebf-8348-96d0808d325e" + + response = test_client.delete( + f"/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions/{id}", + headers=headers, + ) + + assert response.status_code == 204 + assert test_app.db["questions"].count_documents({"status": {"$ne": "deleted"}}) == 1 diff --git a/tests/integration/test_get_question_by_id.py b/tests/integration/test_get_question_by_id.py new file mode 100644 index 0000000..f6af466 --- /dev/null +++ b/tests/integration/test_get_question_by_id.py @@ -0,0 +1,22 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures( + "bids_db_setup_and_teardown", "questions_db_setup_and_teardown" +) + + +def test_get_question_by_id(test_client, api_key): + headers = { + "host": "localhost:8080", + "X-API-Key": api_key, + } + id = "6e7d3f8a-fab3-4ebf-8348-96d0808d325e" + + response = test_client.get( + f"/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions/{id}", + headers=headers, + ) + + assert response.status_code == 200 + assert response.get_json()["_id"] == id diff --git a/tests/integration/test_post_bid.py b/tests/integration/test_post_bid.py index 40de7f0..2318045 100644 --- a/tests/integration/test_post_bid.py +++ b/tests/integration/test_post_bid.py @@ -21,3 +21,6 @@ def test_post_bid(test_app, test_client, basic_jwt): assert response.status_code == 201 assert test_app.db["bids"].count_documents({}) == 3 + assert response.get_json()["_id"] is not None + assert response.get_json()["last_updated"] is not None + assert response.get_json()["links"] is not None diff --git a/tests/integration/test_post_question.py b/tests/integration/test_post_question.py new file mode 100644 index 0000000..d4f2c9b --- /dev/null +++ b/tests/integration/test_post_question.py @@ -0,0 +1,35 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures( + "bids_db_setup_and_teardown", "questions_db_setup_and_teardown" +) + + +def test_post_question(test_app, test_client, basic_jwt): + headers = { + "host": "localhost:8080", + "Content-Type": "application/json", + "Authorization": f"Bearer {basic_jwt}", + } + + data = { + "description": "This is a question", + "question_url": "https://organisation.sharepoint.com/Docs/dummyfolder", + "feedback": { + "description": "This is a description", + "url": "https://organisation.sharepoint.com/Docs/dummyfolder", + }, + } + + response = test_client.post( + f"/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions", + json=data, + headers=headers, + ) + + assert response.status_code == 201 + assert test_app.db["questions"].count_documents({}) == 3 + assert response.get_json()["_id"] is not None + assert response.get_json()["last_updated"] is not None + assert response.get_json()["links"] is not None diff --git a/tests/integration/test_update_question.py b/tests/integration/test_update_question.py new file mode 100644 index 0000000..b0cec8c --- /dev/null +++ b/tests/integration/test_update_question.py @@ -0,0 +1,30 @@ +import pytest + + +pytestmark = pytest.mark.usefixtures( + "bids_db_setup_and_teardown", "questions_db_setup_and_teardown" +) + + +def test_update_question(test_app, test_client, basic_jwt): + headers = { + "host": "localhost:8080", + "Content-Type": "application/json", + "Authorization": f"Bearer {basic_jwt}", + } + + id = "6e7d3f8a-fab3-4ebf-8348-96d0808d325e" + + data = {"description": "THIS IS UPDATED"} + + response = test_client.put( + f"/api/bids/be15c306-c85b-4e67-a9f6-682553c065a1/questions/{id}", + json=data, + headers=headers, + ) + + question = test_app.db["questions"].find_one({"_id": id}) + + assert response.status_code == 200 + assert response.get_json()["description"] == "THIS IS UPDATED" + assert question["description"] == "THIS IS UPDATED" From fdfaecbdf251f4b4f3f66128415334b70f664e21 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 18 Sep 2023 12:45:10 +0100 Subject: [PATCH 204/208] chore: removed redundant test db targets from makefile --- Makefile | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 645e178..a6d05e9 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PYTHON = ./.venv/bin/python3 PIP = ./.venv/bin/pip -.PHONY: help auth clean dbclean mongostart mongostop run setup swag test test-dbclean test-integration test-setup +.PHONY: help auth clean dbclean mongostart mongostop run setup swag test test-integration help: @echo "make help - display this help" @@ -20,9 +20,7 @@ help: @echo "make swag - open swagger documentation" @echo "make setup - setup the application database" @echo "make test - run tests and coverage report" - @echo "make test-dbclean - clear the test database" @echo "make test-integration - run integration tests in test environment" - @echo "make test-setup - setup the test database" @echo "make helptools - display help for tools" auth: @@ -72,26 +70,9 @@ test: @echo "TEST COVERAGE REPORT" coverage report -m --omit="app.py,tests/*,dbconfig/*,custom_formatter.py,conftest.py" -test-dbclean: - @echo "Cleaning up database..." - export TESTING=true; \ - cd ./scripts/; \ - make dbclean; \ - export TESTING= - @echo "Database cleared." - test-integration: pytest tests/integration -vv -s -test-setup: - @echo "Setting up test database..." - export TESTING=true; \ - cd ./scripts/; \ - make dbclean; \ - make bids; \ - make questions; \ - export TESTING= - @echo "Test database setup complete." .PHONY: helptools authplay branch check commit format lint From 0cfe85e859fe3851a0348e9dc33783972d63983f Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 18 Sep 2023 13:41:05 +0100 Subject: [PATCH 205/208] chore: removed unused imports from mongo_setup --- dbconfig/mongo_setup.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index a694840..ea11a99 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -2,11 +2,7 @@ This file contains the configuration for the MongoDB database. """ -import os from pymongo import MongoClient -from dotenv import load_dotenv - -load_dotenv() def get_db(DB_HOST, DB_PORT, DB_NAME): From eac3906de02188032fd4066f53b292b8f08eb214 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 18 Sep 2023 14:02:21 +0100 Subject: [PATCH 206/208] docs: imporved comments in app and dbconfig --- app.py | 3 ++- dbconfig/mongo_setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 4904282..d3f252b 100644 --- a/app.py +++ b/app.py @@ -40,6 +40,7 @@ ) +# App factory function def create_app(): # Create the Flask application app = Flask(__name__) @@ -53,7 +54,7 @@ def create_app(): app.register_blueprint(bid, url_prefix="/api") app.register_blueprint(question, url_prefix="/api") - # Create new client and connect to server in application context + # Create db client instance in application context with app.app_context(): db = get_db(app.config["DB_HOST"], app.config["DB_PORT"], app.config["DB_NAME"]) app.db = db diff --git a/dbconfig/mongo_setup.py b/dbconfig/mongo_setup.py index ea11a99..0d37a5c 100644 --- a/dbconfig/mongo_setup.py +++ b/dbconfig/mongo_setup.py @@ -5,7 +5,7 @@ from pymongo import MongoClient +# Create new client, connect to server and return db instance def get_db(DB_HOST, DB_PORT, DB_NAME): - # Create a new client and connect to the server client = MongoClient(DB_HOST, DB_PORT, serverSelectionTimeoutMS=10000) return client[DB_NAME] From 06e68a61f9c45447eb4411f33cf56363af5ff4c6 Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 25 Sep 2023 12:12:41 +0100 Subject: [PATCH 207/208] chore: cleaned up request examples for demo --- request_examples/invalid_int.http | 28 ------------------- request_examples/invalid_string.http | 11 -------- request_examples/invalid_url.http | 28 ------------------- request_examples/missing_mandatory_field.http | 27 ------------------ .../missing_score_when_has_score_is_true.http | 27 ------------------ 5 files changed, 121 deletions(-) delete mode 100644 request_examples/invalid_int.http delete mode 100644 request_examples/invalid_string.http delete mode 100644 request_examples/invalid_url.http delete mode 100644 request_examples/missing_mandatory_field.http delete mode 100644 request_examples/missing_score_when_has_score_is_true.http diff --git a/request_examples/invalid_int.http b/request_examples/invalid_int.http deleted file mode 100644 index 7c879a4..0000000 --- a/request_examples/invalid_int.http +++ /dev/null @@ -1,28 +0,0 @@ -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": "ONE", - "has_score": true, - "score": 28, - "out_of": 36 - } - ], - "failed": { - "phase": 2, - "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 deleted file mode 100644 index 12d71af..0000000 --- a/request_examples/invalid_string.http +++ /dev/null @@ -1,11 +0,0 @@ -POST http://localhost:8080/api/bids HTTP/1.1 -Content-Type: application/json - -{ - "tender": 42, - "alias": "ONS", - "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/invalid_url.http b/request_examples/invalid_url.http deleted file mode 100644 index d806f3c..0000000 --- a/request_examples/invalid_url.http +++ /dev/null @@ -1,28 +0,0 @@ -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": "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 - } - ], - "failed": { - "phase": 2, - "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 deleted file mode 100644 index 9d39d7a..0000000 --- a/request_examples/missing_mandatory_field.http +++ /dev/null @@ -1,27 +0,0 @@ -POST http://localhost:8080/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 - } - ], - "failed": { - "phase": 2, - "has_score": true, - "score": 20, - "out_of": 36 - } -} \ No newline at end of file diff --git a/request_examples/missing_score_when_has_score_is_true.http b/request_examples/missing_score_when_has_score_is_true.http deleted file mode 100644 index 9e96a2a..0000000 --- a/request_examples/missing_score_when_has_score_is_true.http +++ /dev/null @@ -1,27 +0,0 @@ -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 0204c79a2320c299fa0e38ffc49566c8c7a0533a Mon Sep 17 00:00:00 2001 From: piratejas Date: Mon, 25 Sep 2023 12:25:39 +0100 Subject: [PATCH 208/208] docs: updated readme with test-integ --- README.md | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 91cf88c..39a2554 100644 --- a/README.md +++ b/README.md @@ -8,44 +8,25 @@ This API stores and serves information about Methods bids for client tenders. - Python 3.x - Flask - Homebrew +- Makefile ## Running the API -1. Clone the repository to your local machine: - - ```bash - git clone - ``` -2. Navigate to the root directory of the project: - - ```bash - cd tdse-accessForce-bids-api - ``` -3. Install Makefile if not already installed. You can check if it is installed by running the following command: - - ```bash - make --version - ``` -4. Version 3.81 or higher is required. If you do not have Make installed, you can install it with Homebrew: - - ```bash - brew install make - ``` -5. Run the following command to start the API: +1. Run the following command to start the API: ```bash make run ``` * The API will be available at http://localhost:8080/api/bids -6. To see all available Make targets, run the following command in a new terminal: +2. To see all available Make targets, run the following command in a new terminal: ```bash make help ``` -7. Follow this link to go to the authorization documentation: [Authorization Documentation](https://github.com/methods/tdse-accessForce-auth-stub/blob/main/README.md) +3. Follow this link to go to the authorization documentation: [Authorization Documentation](https://github.com/methods/tdse-accessForce-auth-stub/blob/main/README.md) -8. In a new terminal enter the following command to run authorization server if not already running. This will be needed to generate a token: +4. In a new terminal enter the following command to run authorization server if not already running. This will be needed to generate a token: ```bash make auth @@ -72,7 +53,7 @@ To do this, create a `.env` file in your root folder, with the following key/val DEFAULT_SORT_BIDS=bid_date DEFAULT_SORT_QUESTIONS=description APP_NAME=BidsAPI - APP_VERSION=0.6.0 + APP_VERSION=0.8.0 APP_LANG=Python -------------- @@ -171,6 +152,11 @@ This will perform the following steps: ```bash make test ``` +4. Enter the following command to run the integration tests: + + ```bash + make test-integration + ``` -------------- ## Using auth playground to generate a token and make authenticated requests to the Bids API