Skip to content

Commit

Permalink
Merge pull request #141 from ral-facilities/DSEGOG-311-user-office-re…
Browse files Browse the repository at this point in the history
…st-api

DSEGOG-311 Move User Office API from SOAP to REST
  • Loading branch information
moonraker595 authored Nov 21, 2024
2 parents 4bb91c3 + abe9e3e commit 8ba1a40
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .github/ci_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ auth:
fedid_server_url: ldap://fed.cclrc.ac.uk:389
fedid_server_ldap_realm: FED.CCLRC.AC.UK
experiments:
user_office_wsdl_url: https://devapi.facilities.rl.ac.uk/ws/UserOfficeWebService?wsdl
user_office_rest_api_url: https://devapi.facilities.rl.ac.uk/users-service/v1
scheduler_wsdl_url: https://devapis.facilities.rl.ac.uk/ws/ScheduleWebService?wsdl
# Credentials for user office/scheduler system
username: username
Expand Down
2 changes: 1 addition & 1 deletion operationsgateway_api/config.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ auth:
fedid_server_url: ldap://fed.cclrc.ac.uk:389
fedid_server_ldap_realm: FED.CCLRC.AC.UK
experiments:
user_office_wsdl_url: https://devapi.facilities.rl.ac.uk/ws/UserOfficeWebService?wsdl
user_office_rest_api_url: https://devapi.facilities.rl.ac.uk/users-service/v1
scheduler_wsdl_url: https://devapis.facilities.rl.ac.uk/ws/ScheduleWebService?wsdl
# Credentials for user office/scheduler system
username: username
Expand Down
2 changes: 1 addition & 1 deletion operationsgateway_api/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class ExperimentsConfig(BaseModel):
scheduler_background_frequency: StrictStr
scheduler_background_timezone: StrictStr
scheduler_background_retry_mins: float
user_office_wsdl_url: StrictStr
user_office_rest_api_url: StrictStr
username: StrictStr
password: StrictStr
scheduler_wsdl_url: StrictStr
Expand Down
40 changes: 40 additions & 0 deletions operationsgateway_api/src/experiments/rest_error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from functools import wraps

from requests import HTTPError, RequestException, Timeout
from requests.exceptions import ConnectionError

from operationsgateway_api.src.exceptions import ExperimentDetailsError


def rest_error_handling(endpoint_name):
"""
Parameterised decorator to handle errors raised during the use of the REST API for
the User Office (used to login where the session ID is used to access the Scheduler)
"""

def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except ConnectionError as exc:
raise ExperimentDetailsError("Connection error occurred") from exc
except HTTPError as exc:
raise ExperimentDetailsError(
"A HTTP error occurred when trying to retrieve experiment data from"
f" {endpoint_name} on the external system",
) from exc
except Timeout as exc:
raise ExperimentDetailsError(
f"Request to {endpoint_name} to retrieve experiment data from the"
" external system has timed out",
) from exc
except RequestException as exc:
raise ExperimentDetailsError(
f"Something went wrong when accessing {endpoint_name} to retrieve"
" experiment data from an external system",
) from exc

return wrapper

return decorator
46 changes: 36 additions & 10 deletions operationsgateway_api/src/experiments/scheduler_interface.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from datetime import datetime
import json
import logging
from typing import Dict, List, Union

import requests
from zeep import Client

from operationsgateway_api.src.config import Config
from operationsgateway_api.src.exceptions import ExperimentDetailsError
from operationsgateway_api.src.experiments.rest_error_handling import (
rest_error_handling,
)
from operationsgateway_api.src.experiments.soap_error_handling import (
soap_error_handling,
)
Expand All @@ -19,28 +25,48 @@ class SchedulerInterface:
handling for the calls to the Scheduler too
"""

login_endpoint_name = "/sessions"

def __init__(self) -> None:
self.user_office_client = self.create_user_office_client()
self.scheduler_client = self.create_scheduler_client()
self.session_id = self.login()

@soap_error_handling("create user office client")
def create_user_office_client(self) -> Client:
log.info("Creating user office client")
return Client(Config.config.experiments.user_office_wsdl_url)

@soap_error_handling("create scheduler client")
def create_scheduler_client(self) -> Client:
log.info("Creating scheduler client")
return Client(Config.config.experiments.scheduler_wsdl_url)

@soap_error_handling("login")
@rest_error_handling(login_endpoint_name)
def login(self) -> str:
log.info("Generating session ID for Scheduler system")
return self.user_office_client.service.login(
Config.config.experiments.username,
Config.config.experiments.password,
credentials = {
"username": Config.config.experiments.username,
"password": Config.config.experiments.password,
}
headers = {"Content-Type": "application/json"}

login_response = requests.post(
f"{Config.config.experiments.user_office_rest_api_url}"
f"{SchedulerInterface.login_endpoint_name}",
data=json.dumps(credentials),
headers=headers,
)
if login_response.status_code != 201:
log.error("Request response: %s", login_response.json())
raise ExperimentDetailsError(
"Logging in to retrieve experiments wasn't successful. %s recieved",
login_response.status_code,
)
try:
session_id = login_response.json()["sessionId"]
except KeyError as exc:
log.error("Status code from POST /sessions: %s", login_response.status_code)
log.error("Request response: %s", login_response.json())
raise ExperimentDetailsError(
"Session ID cannot be found from User Office API login endpoint",
) from exc

return session_id

@soap_error_handling("getExperimentDatesForInstrument")
def get_experiment_dates_for_instrument(
Expand Down
34 changes: 34 additions & 0 deletions test/experiments/test_rest_error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from unittest.mock import patch

import pytest
import requests
from requests import ConnectionError, HTTPError, RequestException, Timeout

from operationsgateway_api.src.exceptions import ExperimentDetailsError
from operationsgateway_api.src.experiments.rest_error_handling import (
rest_error_handling,
)


class TestRESTErrorHandling:
@pytest.mark.parametrize(
"raised_exception, expected_exception",
[
pytest.param(ConnectionError, ExperimentDetailsError, id="ConnectionError"),
pytest.param(HTTPError(), ExperimentDetailsError, id="HTTPError"),
pytest.param(Timeout(), ExperimentDetailsError, id="Timeout"),
pytest.param(
RequestException,
ExperimentDetailsError,
id="RequestException (base class for Requests exceptions)",
),
],
)
def test_correct_error_raised(self, raised_exception, expected_exception):
@rest_error_handling("Testing")
def raise_exception():
with patch("requests.get", side_effect=raised_exception):
requests.get("Test URL")

with pytest.raises(expected_exception):
raise_exception()
121 changes: 61 additions & 60 deletions test/experiments/test_scheduler_interface.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
from unittest.mock import patch

import pytest

from operationsgateway_api.src.exceptions import ExperimentDetailsError
from operationsgateway_api.src.experiments.scheduler_interface import SchedulerInterface


class TestSchedulerInterface:
config_instrument_names = ["Test Instrument", "Test Instrument #2"]
config_user_office_username = "Test Username"
config_user_office_password = "Test Password"
config_user_office_wsdl = "Test User Office URL"
config_scheduler_wsdl = "Test Scheduler URL"

@patch(
"operationsgateway_api.src.experiments.scheduler_interface.SchedulerInterface"
".create_user_office_client",
)
@patch(
"operationsgateway_api.src.experiments.scheduler_interface.SchedulerInterface"
".create_scheduler_client",
Expand All @@ -23,29 +19,12 @@ class TestSchedulerInterface:
".login",
return_value="Test Session ID",
)
def test_init(self, _, mock_scheduler_client, mock_user_office_client):
def test_init(self, _, mock_scheduler_client):
test_scheduler_interface = SchedulerInterface()

assert mock_user_office_client.call_count == 1
assert mock_scheduler_client.call_count == 1
assert test_scheduler_interface.session_id == "Test Session ID"

@patch(
"operationsgateway_api.src.config.Config.config.experiments"
".user_office_wsdl_url",
config_user_office_wsdl,
)
@patch("operationsgateway_api.src.experiments.scheduler_interface.Client")
def test_create_user_office_client(self, mock_client):
test_scheduler = object.__new__(SchedulerInterface)
test_scheduler.create_user_office_client()

assert mock_client.call_count == 1

args = mock_client.call_args.args
expected_args = (TestSchedulerInterface.config_user_office_wsdl,)
assert args == expected_args

@patch(
"operationsgateway_api.src.config.Config.config.experiments.scheduler_wsdl_url",
config_scheduler_wsdl,
Expand All @@ -61,42 +40,68 @@ def test_create_scheduler_client(self, mock_client):
expected_args = (TestSchedulerInterface.config_scheduler_wsdl,)
assert args == expected_args

@patch(
"operationsgateway_api.src.config.Config.config.experiments.username",
config_user_office_username,
)
@patch(
"operationsgateway_api.src.config.Config.config.experiments.password",
config_user_office_password,
)
@patch(
"operationsgateway_api.src.experiments.scheduler_interface.SchedulerInterface"
".create_user_office_client",
)
def test_login(self, _):
test_scheduler = object.__new__(SchedulerInterface)
test_scheduler.user_office_client = test_scheduler.create_user_office_client()
@patch("requests.post")
def test_valid_login(self, mock_user_office_login):
mock_user_office_login.return_value.status_code = 201
mock_session_id = "abc1254d-3c54-321a-abc9-a8765b43cd21"
mock_user_office_login.return_value.json.return_value = {
"userId": "1127868",
"sessionId": mock_session_id,
"lastAccessTime": "2024-08-14T08:00:00+01:00",
"loginType": "DATABASE",
"comments": None,
}

with patch.object(test_scheduler, "user_office_client") as mock_client:
# Use object.__new__ so __init__() doesn't run. We don't want to run that as
# we're only testing login()
test_scheduler = object.__new__(SchedulerInterface)
session_id = test_scheduler.login()
assert session_id == mock_session_id

@patch("requests.post")
@pytest.mark.parametrize(
"mock_status_code, mock_json_response",
[
pytest.param(
401,
{
"shortcode": "Unauthorized",
"reason": "Authorization details were wrong",
"details": ["Specified username or password was incorrect"],
},
id="Incorrect credentials",
),
pytest.param(
201,
{
"userId": "1127868",
"lastAccessTime": "2024-08-14T08:00:00+01:00",
"loginType": "DATABASE",
"comments": None,
},
id="Missing session ID",
),
],
)
def test_invalid_login(
self,
mock_user_office_login,
mock_status_code,
mock_json_response,
):
mock_user_office_login.return_value.status_code = mock_status_code
mock_user_office_login.return_value.json.return_value = mock_json_response

with pytest.raises(ExperimentDetailsError):
# Use object.__new__ so __init__() doesn't run. We don't want to run that as
# we're only testing login()
test_scheduler = object.__new__(SchedulerInterface)
test_scheduler.login()
assert mock_client.service.login.call_count == 1

args = mock_client.service.login.call_args.args
expected_args = (
TestSchedulerInterface.config_user_office_username,
TestSchedulerInterface.config_user_office_password,
)

assert args == expected_args

@patch(
"operationsgateway_api.src.config.Config.config.experiments.instrument_names",
config_instrument_names,
)
@patch(
"operationsgateway_api.src.experiments.scheduler_interface.SchedulerInterface"
".create_user_office_client",
)
@patch(
"operationsgateway_api.src.experiments.scheduler_interface.SchedulerInterface"
".create_scheduler_client",
Expand All @@ -106,7 +111,7 @@ def test_login(self, _):
".login",
return_value="Test Session ID",
)
def test_get_experiment_dates_for_instrument(self, _, __, ___):
def test_get_experiment_dates_for_instrument(self, _, __):
date_range = {
"startDate": "2023-03-01T09:00:00",
"endDate": "2023-03-02T09:00:00",
Expand All @@ -125,10 +130,6 @@ def test_get_experiment_dates_for_instrument(self, _, __, ___):
expected_args = ("Test Session ID", "Test Instrument", date_range)
assert args == expected_args

@patch(
"operationsgateway_api.src.experiments.scheduler_interface.SchedulerInterface"
".create_user_office_client",
)
@patch(
"operationsgateway_api.src.experiments.scheduler_interface.SchedulerInterface"
".create_scheduler_client",
Expand All @@ -138,7 +139,7 @@ def test_get_experiment_dates_for_instrument(self, _, __, ___):
".login",
return_value="Test Session ID",
)
def test_get_experiments(self, _, __, ___):
def test_get_experiments(self, _, __):
id_instrument_pairs = [
{"key": 12345678, "value": "Test Instrument Name"},
{"key": 23456789, "value": "Test Instrument Name"},
Expand Down

0 comments on commit 8ba1a40

Please sign in to comment.