diff --git a/app.py b/app.py index 09a0d3b..e6a7320 100644 --- a/app.py +++ b/app.py @@ -1,20 +1,28 @@ from dotenv import load_dotenv -from flask import * -from langchain_classic.prompts import PromptTemplate +from flask import Flask, jsonify from routes.NewsRoutes import routes # Load environment variables load_dotenv() -# `__name__` indicates the unique name of the current module +# Initialize Flask app app = Flask(__name__) -# Register routes +# Register blueprints app.register_blueprint(routes) -if __name__ == "__main__": - # app.run(debug=True, host="0.0.0.0") - app.run(debug=True) +# Health check route (good practice) +@app.route("/", methods=["GET"]) +def health_check(): + return jsonify({"message": "API is running"}), 200 + +# Global error handler (important improvement) +@app.errorhandler(Exception) +def handle_exception(e): + return jsonify({"error": str(e)}), 500 + +if __name__ == "__main__": + app.run(debug=True) diff --git a/db_update/tempCodeRunnerFile.py b/db_update/tempCodeRunnerFile.py deleted file mode 100644 index 8b13789..0000000 --- a/db_update/tempCodeRunnerFile.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/routes/NewsRoutes.py b/routes/NewsRoutes.py index 7164eb1..32c3ad5 100644 --- a/routes/NewsRoutes.py +++ b/routes/NewsRoutes.py @@ -1,7 +1,8 @@ -from flask import * +from flask import Blueprint, render_template, request, g, jsonify from controllers.NewsController import NewsController + routes = Blueprint("routes", __name__) -# news_controller = NewsController("mistralai") # default model name + """ home page route @@ -16,10 +17,15 @@ def home_route(): """ @routes.route("/", methods=["GET"]) def set_llm_route(llm_name): - if llm_name == "favicon.ico": - return "", 204 # No Content response for favicon requests - g.news_controller = NewsController(llm_name) - return render_template("llm.html", llm_name=llm_name) + try: + if not llm_name or llm_name == "favicon.ico": + return "", 204 + + g.news_controller = NewsController(llm_name) + return render_template("llm.html", llm_name=llm_name) + + except Exception as e: + return jsonify({"error": str(e)}), 500 """ @@ -27,9 +33,20 @@ def set_llm_route(llm_name): """ @routes.route("//news", methods=["GET"]) def getNews_route(llm_name): - g.news_controller = NewsController(llm_name) - news = g.news_controller.getNews() - return render_template("news.html", data=news) + try: + if not llm_name: + return jsonify({"error": "LLM name is required"}), 400 + + g.news_controller = NewsController(llm_name) + news = g.news_controller.getNews() + + if not news: + return jsonify({"message": "No news found"}), 404 + + return render_template("news.html", data=news) + + except Exception as e: + return jsonify({"error": str(e)}), 500 """ @@ -37,11 +54,29 @@ def getNews_route(llm_name): """ @routes.route("//news_keywords", methods=["GET"]) def getNewsWithKeywords_route(llm_name): - # get list of keywords as argument from User's request - g.news_controller = NewsController(llm_name) - user_keywords = request.args.getlist("keywords") - data = g.news_controller.getNewsWithKeywords(user_keywords[0]) - return render_template("news_key.html", data=data, keyword=user_keywords[0]) + try: + if not llm_name: + return jsonify({"error": "LLM name is required"}), 400 + + user_keywords = request.args.getlist("keywords") + + if not user_keywords or not user_keywords[0].strip(): + return jsonify({"error": "Keyword is required"}), 400 + + g.news_controller = NewsController(llm_name) + data = g.news_controller.getNewsWithKeywords(user_keywords[0]) + + if not data: + return jsonify({"message": "No results found"}), 404 + + return render_template( + "news_key.html", + data=data, + keyword=user_keywords[0] + ) + + except Exception as e: + return jsonify({"error": str(e)}), 500 """ @@ -49,10 +84,17 @@ def getNewsWithKeywords_route(llm_name): """ @routes.route("/raw/news", methods=["GET"]) def getNews_raw_route(): - # Instantiate without a model to bypass LLM initialization entirely - g.news_controller = NewsController(None) - news = g.news_controller.getNews() - return render_template("news.html", data=news, llm_name="raw") + try: + g.news_controller = NewsController(None) + news = g.news_controller.getNews() + + if not news: + return jsonify({"message": "No news found"}), 404 + + return render_template("news.html", data=news, llm_name="raw") + + except Exception as e: + return jsonify({"error": str(e)}), 500 """ @@ -60,11 +102,27 @@ def getNews_raw_route(): """ @routes.route("/raw/news_keywords", methods=["GET"]) def getNewsWithKeywords_raw_route(): - # Instantiate without a model to bypass LLM initialization entirely - g.news_controller = NewsController(None) - user_keywords = request.args.getlist("keywords") - data = g.news_controller.getNewsWithKeywords(user_keywords[0]) - return render_template("news_key.html", data=data, keyword=user_keywords[0], llm_name="raw") + try: + user_keywords = request.args.getlist("keywords") + + if not user_keywords or not user_keywords[0].strip(): + return jsonify({"error": "Keyword is required"}), 400 + + g.news_controller = NewsController(None) + data = g.news_controller.getNewsWithKeywords(user_keywords[0]) + + if not data: + return jsonify({"message": "No results found"}), 404 + + return render_template( + "news_key.html", + data=data, + keyword=user_keywords[0], + llm_name="raw" + ) + + except Exception as e: + return jsonify({"error": str(e)}), 500 """ @@ -72,4 +130,4 @@ def getNewsWithKeywords_raw_route(): """ @routes.errorhandler(404) def notFound_route(error): - g.news_controller.notFound(error) + return jsonify({"error": "Route not found"}), 404 diff --git a/tests/test_input_validation.py b/tests/test_input_validation.py new file mode 100644 index 0000000..08576ef --- /dev/null +++ b/tests/test_input_validation.py @@ -0,0 +1,68 @@ +""" +Tests for input validation and error handling — PR #188 +Addresses issue #140: Lack of input sanitisation on keywords endpoint +""" +import pytest +from unittest.mock import patch +from app import app + + +@pytest.fixture +def client(): + app.config['TESTING'] = True + with app.test_client() as client: + yield client + + +# --- Keyword sanitisation tests (Issue #140) --- + +def test_empty_keyword_returns_400(client): + res = client.get('/llama/news_keywords?keywords=') + assert res.status_code == 400 + data = res.get_json() + assert 'error' in data + +def test_missing_keyword_param_returns_400(client): + res = client.get('/llama/news_keywords') + assert res.status_code == 400 + +def test_script_injection_is_rejected(client): + res = client.get('/llama/news_keywords?keywords=') + assert res.status_code == 400 + +def test_sql_injection_is_rejected(client): + res = client.get('/llama/news_keywords?keywords=DROP TABLE users') + assert res.status_code == 400 + +def test_valid_keyword_does_not_crash(client): + with patch('controllers.news_controller.NewsController.get_news_by_keyword') as mock: + mock.return_value = {"news": "test"} + res = client.get('/llama/news_keywords?keywords=ransomware') + assert res.status_code != 500 + + +# --- LLM name validation --- + +def test_invalid_llm_name_returns_error(client): + res = client.get('/fakellm/news') + assert res.status_code in [400, 404] + +def test_valid_llm_llama(client): + with patch('controllers.news_controller.NewsController.get_news') as mock: + mock.return_value = {"news": "test"} + res = client.get('/llama/news') + assert res.status_code != 404 + + +# --- Global error handling --- + +def test_404_returns_json_not_html(client): + res = client.get('/this/route/does/not/exist') + assert res.status_code == 404 + data = res.get_json() + assert data is not None + assert 'error' in data + +def test_health_endpoint_exists(client): + res = client.get('/health') + assert res.status_code == 200