diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e38be6..922ff898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ RELEASING: 14. Create new release in GitHub with tag version and release title of `vX.X.X` --> +## Unreleased +### Added +- API key validation for selected provider ([#339](https://github.com/GIScience/orstools-qgis-plugin/issues/339)) + ## [2.0.1] - 2025-06-01 ### Added - Readd old icons and add new icons for processing algorithms diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py index 3d651098..dfad6764 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -32,7 +32,7 @@ from datetime import datetime from typing import Optional -from qgis.PyQt.QtWidgets import QCheckBox +from qgis.PyQt.QtNetwork import QNetworkRequest from ..utils.router import route_as_layer @@ -57,11 +57,20 @@ Qgis, # noqa: F811 QgsAnnotation, QgsCoordinateTransform, + QgsBlockingNetworkRequest, ) from qgis.gui import QgsMapCanvasAnnotationItem, QgsCollapsibleGroupBox, QgisInterface -from qgis.PyQt.QtCore import QSizeF, QPointF, QCoreApplication +from qgis.PyQt.QtCore import QSizeF, QPointF, QCoreApplication, QUrl from qgis.PyQt.QtGui import QTextDocument -from qgis.PyQt.QtWidgets import QAction, QDialog, QApplication, QMenu, QMessageBox, QDialogButtonBox +from qgis.PyQt.QtWidgets import ( + QAction, + QDialog, + QApplication, + QMenu, + QMessageBox, + QDialogButtonBox, + QCheckBox, +) from qgis.PyQt.QtGui import QColor from qgis.PyQt.QtWidgets import ( QWidget, @@ -385,6 +394,7 @@ def __init__(self, iface: QgisInterface, parent=None) -> None: self.provider_config.setIcon(gui.GuiUtils.get_icon("icon_settings.png")) self.about_button.setIcon(gui.GuiUtils.get_icon("icon_about.png")) self.help_button.setIcon(gui.GuiUtils.get_icon("icon_help.png")) + self.check_key_button.setIcon(gui.GuiUtils.get_icon("icon_key.png")) # Connect signals to the color_duplicate_items function self.routing_fromline_list.model().rowsRemoved.connect( @@ -396,6 +406,8 @@ def __init__(self, iface: QgisInterface, parent=None) -> None: self.load_provider_combo_state() self.provider_combo.activated.connect(self.save_selected_provider_state) + self.provider_combo.currentIndexChanged.connect(self.set_check_provider_button) + self.check_key_button.clicked.connect(self.check_api_key) advanced_boxes = self.advances_group.findChildren(QgsCollapsibleGroupBox) for box in advanced_boxes: @@ -408,6 +420,43 @@ def __init__(self, iface: QgisInterface, parent=None) -> None: self.rubber_band = None + def set_check_provider_button(self): + """Checks, whether the current provider is of the public API and set a check provider if so""" + provider = configmanager.read_config()["providers"][self.provider_combo.currentIndex()] + url = provider.get("base_url", "") + if url == "https://api.openrouteservice.org": + self.check_key_button.show() + else: + self.check_key_button.hide() + + def check_api_key(self) -> None: + provider = configmanager.read_config()["providers"][self.provider_combo.currentIndex()] + api_key = provider.get("key", "") + + if not api_key: + QMessageBox.critical( + self, self.tr("Error"), self.tr("No API key set for the selected provider.") + ) + return + + url = f"https://api.openrouteservice.org/v2/directions/driving-car?api_key={api_key}&start=8.681495,49.41461&end=8.687872,49.420318" + request = QgsBlockingNetworkRequest() + qrequest = QNetworkRequest(QUrl(url)) + qrequest.setRawHeader( + b"Accept", + b"application/json, application/geo+json, application/gpx+xml, img/png; charset=utf-8", + ) + error_code = request.get(qrequest) + + if error_code == QgsBlockingNetworkRequest.ErrorCode.NoError: + QMessageBox.information(self, self.tr("Success"), self.tr("API key check successful!")) + else: + QMessageBox.critical( + self, + self.tr("API key Error"), + self.tr(f"API key check failed, key: {api_key} is invalid."), + ) + def _save_vertices_to_layer(self) -> None: """Saves the vertices list to a temp layer""" items = [ diff --git a/ORStools/gui/ORStoolsDialogUI.ui b/ORStools/gui/ORStoolsDialogUI.ui index 4b48c8a2..528e7292 100644 --- a/ORStools/gui/ORStoolsDialogUI.ui +++ b/ORStools/gui/ORStoolsDialogUI.ui @@ -169,6 +169,13 @@ + + + + Check if the API key for the selected provider is valid + + + diff --git a/ORStools/gui/img/icon_key.png b/ORStools/gui/img/icon_key.png new file mode 100644 index 00000000..32f8298f Binary files /dev/null and b/ORStools/gui/img/icon_key.png differ diff --git a/tests/test_gui.py b/tests/test_gui.py index d51a1728..bd5883df 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from qgis.PyQt.QtWidgets import QLineEdit from qgis.core import QgsSettings from qgis.gui import QgsCollapsibleGroupBox @@ -5,7 +7,7 @@ from qgis.PyQt.QtTest import QTest from qgis.PyQt.QtCore import Qt, QEvent, QPoint -from qgis.PyQt.QtWidgets import QPushButton +from qgis.PyQt.QtWidgets import QPushButton, QMessageBox from qgis.gui import QgsMapCanvas, QgsMapMouseEvent, QgsRubberBand from qgis.core import ( QgsCoordinateReferenceSystem, @@ -358,3 +360,51 @@ def test_ORStoolsDialogConfig_url(self): "POINT(8.67251100000000008 49.39887900000000087)", next(layer.getFeatures()).geometry().asPolyline()[0].asWkt(), ) + + def test_check_key_button(self): + from ORStools.gui.ORStoolsDialog import ORStoolsDialog + from ORStools.gui.ORStoolsDialogConfig import ORStoolsDialogConfigMain + + CRS = QgsCoordinateReferenceSystem.fromEpsgId(3857) + CANVAS.setExtent(QgsRectangle(258889, 7430342, 509995, 7661955)) + CANVAS.setDestinationCrs(CRS) + CANVAS.setFrameStyle(0) + CANVAS.resize(600, 400) + self.assertEqual(CANVAS.width(), 600) + self.assertEqual(CANVAS.height(), 400) + + dlg = ORStoolsDialog(IFACE) + dlg.open() + self.assertTrue(dlg.isVisible()) + + # Set and reset config to test whether the reset works + dlg_config = ORStoolsDialogConfigMain() + provider = dlg_config.providers.findChildren(QgsCollapsibleGroupBox)[0] + + # set endpoint of directions to non-existent value + line_edit = provider.findChild(QLineEdit, "openrouteservice_key_text") + api_key = line_edit.text() + line_edit.setText("notanapikey") + dlg_config.accept() + + settings_directions_endpoint = QgsSettings().value("ORStools/config")["providers"][0][ + "key" + ] + + self.assertEqual(settings_directions_endpoint, "notanapikey") + + with patch.object(QMessageBox, "information", return_value=QMessageBox.Ok) as mock_info, \ + patch.object(QMessageBox, "critical", return_value=QMessageBox.Ok) as mock_crit: + dlg.check_key_button.clicked.emit() + + dlg_config = ORStoolsDialogConfigMain() + provider = dlg_config.providers.findChildren(QgsCollapsibleGroupBox)[0] + + line_edit = provider.findChild(QLineEdit, "openrouteservice_key_text") + line_edit.setText(api_key) + dlg_config.accept() + + dlg.check_key_button.clicked.emit() + + mock_info.assert_called_once() + mock_crit.assert_called_once()