From d3b1a655deb954e90d33846b9e4d4c30e9c20d18 Mon Sep 17 00:00:00 2001 From: Pelle Koster <pelle.koster@geant.org> Date: Thu, 11 Jul 2024 08:11:59 +0200 Subject: [PATCH] Introduce better grafana mocking for testing --- brian_dashboard_manager/app.py | 3 +- brian_dashboard_manager/config.py | 2 +- brian_dashboard_manager/grafana/dashboard.py | 23 +- .../grafana/organization.py | 2 +- brian_dashboard_manager/grafana/provision.py | 4 +- .../inventory_provider/interfaces.py | 8 +- brian_dashboard_manager/routes/update.py | 2 +- test/conftest.py | 310 +++++++++++++++++- test/test_grafana_dashboard.py | 205 +++++------- test/test_grafana_datasource.py | 77 +---- test/test_grafana_folder.py | 37 +-- test/test_grafana_organization.py | 104 ++---- test/test_update.py | 145 +------- tox.ini | 3 +- 14 files changed, 473 insertions(+), 452 deletions(-) diff --git a/brian_dashboard_manager/app.py b/brian_dashboard_manager/app.py index 5bc14b9..347dbee 100644 --- a/brian_dashboard_manager/app.py +++ b/brian_dashboard_manager/app.py @@ -2,9 +2,8 @@ default app creation """ import brian_dashboard_manager -from brian_dashboard_manager import environment, CONFIG_KEY +from brian_dashboard_manager import CONFIG_KEY -environment.setup_logging() app = brian_dashboard_manager.create_app() if __name__ == "__main__": diff --git a/brian_dashboard_manager/config.py b/brian_dashboard_manager/config.py index b22ef4e..81e4720 100644 --- a/brian_dashboard_manager/config.py +++ b/brian_dashboard_manager/config.py @@ -148,7 +148,7 @@ DEFAULT_ORGANIZATIONS = [ ] CONFIG_SCHEMA = { - "$schema": "https://json-schema.org/draft-07/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "influx-datasource": { diff --git a/brian_dashboard_manager/grafana/dashboard.py b/brian_dashboard_manager/grafana/dashboard.py index 37b6716..74723ee 100644 --- a/brian_dashboard_manager/grafana/dashboard.py +++ b/brian_dashboard_manager/grafana/dashboard.py @@ -55,7 +55,7 @@ def delete_dashboard(request: TokenRequest, dashboard: dict, folder_id=None): dash = _search_dashboard(request, dashboard, folder_id) if dash is None: return True - uid = dash.get('dashboard', {}).get('uid', '') + uid = dash.get('uid', '') if uid: return _delete_dashboard(request, uid) else: @@ -188,7 +188,7 @@ def _get_dashboard(request: TokenRequest, uid): r = request.get(f'api/dashboards/uid/{uid}') except HTTPError: return None - return r.json() + return r.json()['dashboard'] def create_dashboard(request: TokenRequest, dashboard: dict, folder_id=None): @@ -211,7 +211,7 @@ def create_dashboard(request: TokenRequest, dashboard: dict, folder_id=None): # The title might not match the one that's provisioned with that UID. # Try to find it by searching for the title instead. if existing_dashboard is not None: - grafana_title = existing_dashboard['dashboard']['title'] + grafana_title = existing_dashboard['title'] different = grafana_title != title else: different = False @@ -220,12 +220,12 @@ def create_dashboard(request: TokenRequest, dashboard: dict, folder_id=None): existing_dashboard = _search_dashboard(request, dashboard, folder_id) if existing_dashboard: - dashboard['uid'] = existing_dashboard['dashboard']['uid'] - dashboard['id'] = existing_dashboard['dashboard']['id'] - dashboard['version'] = existing_dashboard['dashboard']['version'] + dashboard['uid'] = existing_dashboard['uid'] + dashboard['id'] = existing_dashboard['id'] + dashboard['version'] = existing_dashboard['version'] else: # We are creating a new dashboard, delete ID if it exists. - del dashboard['id'] + dashboard.pop('id', None) payload = { 'dashboard': dashboard, @@ -241,14 +241,17 @@ def create_dashboard(request: TokenRequest, dashboard: dict, folder_id=None): return r.json() except HTTPError as e: message = '' - if e.response is not None and e.response.status_code < 500: + if e.response is not None: # log the error message from Grafana - message = e.response.json() + try: + message = e.response.json() + except json.JSONDecodeError: + message = e.response.text + logger.exception(f"Error when provisioning dashboard {title}: {message}") # only retry on server side errors if e.response is not None and e.response.status_code < 500: break - logger.exception(f'Error when provisioning dashboard {title}: {message}') time.sleep(1) # sleep for 1 second before retrying return None diff --git a/brian_dashboard_manager/grafana/organization.py b/brian_dashboard_manager/grafana/organization.py index 32d4583..629fe8a 100644 --- a/brian_dashboard_manager/grafana/organization.py +++ b/brian_dashboard_manager/grafana/organization.py @@ -104,7 +104,7 @@ def delete_api_token(request: AdminRequest, token_id: int, org_id=None): :return: delete response """ - assert token_id + assert token_id is not None if org_id: switch_active_organization(request, org_id) result = request.delete(f'api/auth/keys/{token_id}') diff --git a/brian_dashboard_manager/grafana/provision.py b/brian_dashboard_manager/grafana/provision.py index 8087026..aeab79e 100644 --- a/brian_dashboard_manager/grafana/provision.py +++ b/brian_dashboard_manager/grafana/provision.py @@ -725,7 +725,7 @@ def provision_maybe(config): write_timestamp(now.timestamp(), False) -def provision(config): +def provision(config, raise_exceptions=False): """ The entrypoint for the provisioning process. @@ -827,6 +827,8 @@ def provision(config): delete_api_token(request, token['id'], org_id=org_id) except Exception: logger.exception(f'Error when provisioning org {org["name"]}') + if raise_exceptions: + raise break logger.info(f'Time to complete: {time.time() - start}') diff --git a/brian_dashboard_manager/inventory_provider/interfaces.py b/brian_dashboard_manager/inventory_provider/interfaces.py index 4299694..f37adb6 100644 --- a/brian_dashboard_manager/inventory_provider/interfaces.py +++ b/brian_dashboard_manager/inventory_provider/interfaces.py @@ -62,7 +62,7 @@ _PORT_TYPES = [t.name for t in list(PORT_TYPES)] _INTERFACE_TYPES = [i.name for i in list(INTERFACE_TYPES)] ROUTER_INTERFACES_SCHEMA = { - "$schema": "https://json-schema.org/draft-07/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "array", "items": { "type": "object", @@ -89,7 +89,7 @@ ROUTER_INTERFACES_SCHEMA = { } INTERFACE_LIST_SCHEMA = { - '$schema': 'https://json-schema.org/draft-07/schema#', + '$schema': 'http://json-schema.org/draft-07/schema#', 'definitions': { 'service': { @@ -137,7 +137,7 @@ INTERFACE_LIST_SCHEMA = { } GWS_DIRECT_DATA_SCHEMA = { - '$schema': 'https://json-schema.org/draft-07/schema#', + '$schema': 'http://json-schema.org/draft-07/schema#', 'definitions': { 'oid': { @@ -214,7 +214,7 @@ GWS_DIRECT_DATA_SCHEMA = { } MULTICAST_SUBSCRIPTION_LIST_SCHEMA = { - '$schema': 'https://json-schema.org/draft-07/schema#', + '$schema': 'http://json-schema.org/draft-07/schema#', 'definitions': { 'ipv4-address': { diff --git a/brian_dashboard_manager/routes/update.py b/brian_dashboard_manager/routes/update.py index 56ada82..4674d82 100644 --- a/brian_dashboard_manager/routes/update.py +++ b/brian_dashboard_manager/routes/update.py @@ -13,7 +13,7 @@ from brian_dashboard_manager.config import STATE_PATH routes = Blueprint("update", __name__) UPDATE_RESPONSE_SCHEMA = { - '$schema': 'https://json-schema.org/draft-07/schema#', + '$schema': 'http://json-schema.org/draft-07/schema#', 'type': 'object', 'properties': { 'message': { diff --git a/test/conftest.py b/test/conftest.py index 81a61e6..f071048 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,8 +1,16 @@ +import copy +import datetime +import itertools import json import os +import re +import string import tempfile +from brian_dashboard_manager import environment +from brian_dashboard_manager.grafana.utils.request import TokenRequest import pytest import brian_dashboard_manager +import responses @pytest.fixture @@ -69,7 +77,305 @@ def data_config_filename(data_config): @pytest.fixture -def client(data_config_filename): - os.environ['CONFIG_FILENAME'] = data_config_filename +def client(mocker, data_config_filename): + mocker.patch.object(environment, "setup_logging") + os.environ["CONFIG_FILENAME"] = data_config_filename with brian_dashboard_manager.create_app().test_client() as c: yield c + + +@pytest.fixture +def mock_grafana(data_config): + def uid_generator(): + return ( + "".join(result) + for result in itertools.permutations(string.ascii_lowercase, 8) + ) + + class MockGrafana: + def __init__(self) -> None: + self.folders = {} + self.dashboards = {} + self.datasources = {} + self.organizations = {} + self.api_tokens = {} + self.uids = uid_generator() + self.ids = itertools.count(start=1) + self.request = TokenRequest(data_config["hostname"], token="abc") + + def list_api_tokens(self): + return list(copy.copy(val) for val in self.api_tokens.values()) + + def create_api_token(self, payload): + lifetime = payload.get("secondsToLive") + return self._create_object( + { + **payload, + "key": "key", + "expiration": ( + ( + datetime.datetime.utcnow() + datetime.timedelta(lifetime) + ).strftime("%Y-%m-%dT%H:%M:%SZ") + if lifetime is not None + else None + ), + }, + self.api_tokens, + ) + + def delete_api_token(self, uid): + return self._delete_object(uid, self.api_tokens, name="api token") + + def list_organizations(self): + return list(copy.copy(val) for val in self.organizations.values()) + + def create_organization(self, organization): + organization = { + **organization, + "id": next(self.ids), + "uid": str(organization.get("uid") or next(self.uids)), + } + self.organizations[organization["uid"]] = organization + return { + "orgId": organization["id"], + "message": "Organization created", + } + + def delete_organization(self, uid): + return self._delete_object(uid, self.organizations, name="organization") + + def list_datasources(self): + return list(copy.copy(val) for val in self.datasources.values()) + + def create_datasource(self, datasource): + return self._create_object(datasource, self.datasources) + + def delete_datasource(self, uid): + return self._delete_object(uid, self.datasources, name="datasource") + + def list_folders(self): + return list(self.folders.values()) + + def create_folder(self, folder): + return self._create_object(folder, self.folders) + + def delete_folder(self, uid): + return self._delete_object(uid, self.folders, name="folder") + + def list_dashboards(self, title=None, folder_id=None): + return [ + copy.copy(db) + for db in self.dashboards.values() + if (title is None or db["title"] == title) + and (folder_id is None or db.get("folderId") == folder_id) + ] + + def create_dashboard(self, dashboard): + return self._create_object(dashboard, self.dashboards) + + def delete_dashboard(self, uid): + return self._delete_object(uid, self.dashboards, name="dashboard") + + def _create_object(self, obj, all_objects): + id = obj.get("id") + uid = obj.get("uid") + obj = { + **obj, + "id": id if id is not None else next(self.ids), + "uid": str(uid if uid is not None else next(self.uids)), + } + all_objects[obj["uid"]] = obj + return obj + + def _delete_object(self, uid, all_objects, name="object"): + del all_objects[uid] + return {"message": f"deleted {name}"} + + grafana = MockGrafana() + + url_prefix = f".*{data_config['hostname']}/" + + def json_request_cb(fun): + def _wrapped(request): + payload = json.loads(request.body) if request.body else None + status, result = fun(payload, request) + return (status, {}, json.dumps(result)) + + return _wrapped + + # --- Api Tokens --- + responses.add_callback( + method=responses.GET, + url=re.compile(url_prefix + r"api/auth/keys?.+$"), + callback=json_request_cb(lambda p, b: (200, grafana.list_api_tokens())), + ) + + @json_request_cb + def create_api_token_callback(payload, request): + return (200, grafana.create_api_token(payload)) + + responses.add_callback( + method=responses.POST, + url=re.compile(url_prefix + "api/auth/keys"), + callback=create_api_token_callback, + ) + + @json_request_cb + def delete_api_token_callback(payload, request): + id = int(request.path_url.split("/")[-1]) + for uid, ds in grafana.api_tokens.items(): + if ds["id"] == id: + return (200, grafana.delete_api_token(uid)) + return (404, "") + + responses.add_callback( + method=responses.DELETE, + url=re.compile(url_prefix + r"api/auth/keys/.+$"), + callback=delete_api_token_callback, + ) + # --- Organizations --- + responses.add_callback( + method=responses.GET, + url=re.compile(url_prefix + "api/orgs"), + callback=json_request_cb(lambda p, b: (200, grafana.list_organizations())), + ) + + @json_request_cb + def create_organization_callback(body, _): + return (200, grafana.create_organization(body)) + + responses.add_callback( + method=responses.POST, + url=re.compile(url_prefix + "api/orgs"), + callback=create_organization_callback, + ) + + # --- Datasources --- + responses.add_callback( + method=responses.GET, + url=re.compile(url_prefix + "api/datasources"), + callback=json_request_cb(lambda p, b: (200, grafana.list_datasources())), + ) + + @json_request_cb + def create_datasource_callback(payload, _): + return (200, grafana.create_datasource(payload)) + + responses.add_callback( + method=responses.POST, + url=re.compile(url_prefix + "api/datasources"), + callback=create_datasource_callback, + ) + + @json_request_cb + def delete_datasource_callback(payload, request): + name = request.path_url.split("/")[-1] + for uid, ds in grafana.datasources.items(): + if ds["name"] == name: + return (200, {}, grafana.delete_datasource(uid)) + return (404, {}, "") + + responses.add_callback( + method=responses.DELETE, + url=re.compile(url_prefix + r"api/datasources/name/.+$"), + callback=delete_datasource_callback, + ) + # --- Folders --- + responses.add_callback( + method=responses.GET, + url=re.compile(url_prefix + "api/folders"), + callback=json_request_cb(lambda p, b: (200, grafana.list_folders())), + ) + + @json_request_cb + def create_folder_callback(payload, _): + return (200, grafana.create_folder(payload)) + + responses.add_callback( + method=responses.POST, + url=re.compile(url_prefix + "api/folders"), + callback=create_folder_callback, + ) + + @json_request_cb + def delete_folder_callback(payload, request): + uid = request.path_url.split("/")[-1] + try: + return (200, grafana.delete_folder(uid)) + except KeyError: + return (404, {}) + + responses.add_callback( + method=responses.DELETE, + url=re.compile(url_prefix + r"api/folders/.+$"), + callback=delete_folder_callback, + ) + + # --- Dashboards --- + @json_request_cb + def get_dashboard_callback(payload, request): + uid = request.path_url.split("/")[-1] + try: + return (200, {"dashboard": grafana.dashboards[uid]}) + except KeyError: + return (404, {}) + + responses.add_callback( + method=responses.GET, + url=re.compile(url_prefix + r"api/dashboards/uid/.+$"), + callback=get_dashboard_callback, + ) + + @json_request_cb + def search_dashboard_callback(_, request): + query = request.params + print(query) + return ( + 200, + grafana.list_dashboards(query.get("title"), query.get("folderIds")), + ) + + responses.add_callback( + method=responses.GET, + url=re.compile(url_prefix + "api/search"), + callback=search_dashboard_callback, + ) + + @json_request_cb + def create_dashboard_callback(payload, request): + dashboard = payload["dashboard"] + + return (200, grafana.create_dashboard(dashboard)) + + responses.add_callback( + method=responses.POST, + url=re.compile(url_prefix + "api/dashboards/db"), + callback=create_dashboard_callback, + ) + + @json_request_cb + def delete_dashboard_callback(payload, request): + uid = request.path_url.split("/")[-1] + try: + return (200, grafana.delete_dashboard(uid)) + except KeyError: + return (404, {}) + + responses.add_callback( + method=responses.DELETE, + url=re.compile(url_prefix + r"api/dashboards/uid/.+$"), + callback=delete_dashboard_callback, + ) + + # --- Other --- + responses.add( + method=responses.PUT, + url=re.compile(url_prefix + "api/org/preferences"), + json={"message": "Preferences updated"}, + ) + responses.add( + method=responses.POST, + url=re.compile(url_prefix + r"api/user/using/.+$"), + json={"message": "ok"}, + ) + return grafana diff --git a/test/test_grafana_dashboard.py b/test/test_grafana_dashboard.py index e3da83f..c053410 100644 --- a/test/test_grafana_dashboard.py +++ b/test/test_grafana_dashboard.py @@ -1,130 +1,73 @@ -import pytest -import json -import requests import responses from brian_dashboard_manager.grafana import dashboard, provision from brian_dashboard_manager.grafana.utils.request import TokenRequest @responses.activate -def test_get_dashboard(data_config): +def test_get_dashboard(mock_grafana): + mock_grafana.create_dashboard({"uid": "1"}) - UID = 1 + request = mock_grafana.request - request = TokenRequest(**data_config, token='test') - - responses.add_callback( - method=responses.GET, - url=request.BASE_URL + - f'api/dashboards/uid/{UID}', - callback=lambda f: (404, {}, '')) + data = dashboard._get_dashboard(request, "1") + assert data["uid"] == "1" - data = dashboard._get_dashboard(request, UID) + data = dashboard._get_dashboard(request, "2") assert data is None - responses.add( - method=responses.GET, - url=request.BASE_URL + f'api/dashboards/uid/{UID+1}', - json={'uid': 1}) - - data = dashboard._get_dashboard(request, UID + 1) - assert data['uid'] == 1 - @responses.activate -def test_delete_dashboards(data_config): - UID = 1 - dashboards = [{'uid': UID}] +def test_delete_dashboards_new(mock_grafana): + mock_grafana.create_dashboard({"uid": "1"}) + mock_grafana.create_dashboard({"uid": "2"}) - request = TokenRequest(**data_config, token='test') + request = mock_grafana.request + assert dashboard.delete_dashboards(request) is True + assert not mock_grafana.dashboards - responses.add( - method=responses.GET, - url=request.BASE_URL + f'api/dashboards/uid/{UID}', - json=dashboards[0]) - - responses.add( - method=responses.GET, - url=request.BASE_URL + 'api/search', - json=dashboards) - - def delete_callback(request): - uid = request.path_url.split('/')[-1] - assert int(uid) == UID - return 200, {}, json.dumps({'message': 'Dashboard has been deleted.'}) - - responses.add_callback( - method=responses.DELETE, - url=request.BASE_URL + - f'api/dashboards/uid/{UID}', - callback=delete_callback) - data = dashboard.delete_dashboards(request) - assert data is True - - responses.add_callback( - method=responses.DELETE, - url=request.BASE_URL + - f'api/dashboards/uid/{UID+1}', - callback=lambda f: (400, {}, '')) - - with pytest.raises(requests.HTTPError): - data = dashboard._delete_dashboard(request, UID + 1) +@responses.activate +def test_delete_nonexsiting_dashboard(mock_grafana): + assert not mock_grafana.dashboards + assert dashboard._delete_dashboard(mock_grafana.request, "2") is True @responses.activate -def test_delete_dashboard(data_config): - UID = 1 - ID = 1 +def test_delete_dashboard_by_uid(mock_grafana): + UID = "1" VERSION = 1 - FOLDER_ID = 1 - TITLE = 'testdashboard' - dash = {'id': ID, 'uid': UID, 'title': TITLE, 'version': VERSION} - request = TokenRequest(**data_config, token='test') - - responses.add( - method=responses.DELETE, - url=request.BASE_URL + f'api/dashboards/uid/{UID}', - json={'message': 'deleted dashboard'}) - - responses.add( - method=responses.GET, - url=request.BASE_URL + f'api/dashboards/uid/{UID}', - json={}) - - responses.add( - method=responses.GET, - url=request.BASE_URL + 'api/search', - json=[dash]) + TITLE = "testdashboard" + dash = {"uid": UID, "title": TITLE, "version": VERSION} + mock_grafana.create_dashboard(dash) + request = mock_grafana.request - deleted = dashboard.delete_dashboard(request, dash) - assert deleted - del dash['uid'] - deleted = dashboard.delete_dashboard(request, dash, FOLDER_ID) - assert deleted + assert mock_grafana.dashboards + assert dashboard.delete_dashboard(request, dash) + assert not mock_grafana.dashboards @responses.activate -def test_search_dashboard(data_config): - UID = 1 - TITLE = 'testdashboard' - dashboards = [{'uid': UID, 'title': TITLE}] +def test_delete_dashboard_by_title_and_folder(mock_grafana): + FOLDER_ID = 1 + TITLE = "testdashboard" + dash = {"title": TITLE, "folderId": FOLDER_ID} + mock_grafana.create_dashboard(dash) + request = mock_grafana.request - request = TokenRequest(**data_config, token='test') + assert mock_grafana.dashboards + assert dashboard.delete_dashboard(request, dash) + assert not mock_grafana.dashboards - responses.add( - method=responses.GET, - url=request.BASE_URL + 'api/search', - json=dashboards) - responses.add( - method=responses.GET, - url=request.BASE_URL + f'api/dashboards/uid/{UID}', - json=dashboards[0]) +@responses.activate +def test_search_dashboard(mock_grafana): + UID = "1" + TITLE = "testdashboard" + mock_grafana.create_dashboard({"uid": UID, "title": TITLE}) + request = mock_grafana.request - data = dashboard._search_dashboard( - request, {'title': dashboards[0]['title']}) - assert data['uid'] == UID + data = dashboard._search_dashboard(request, {"title": TITLE}) + assert data["uid"] == UID data = dashboard._search_dashboard(request, {'title': 'DoesNotExist'}) assert data is None @@ -144,40 +87,48 @@ def test_search_dashboard_error(data_config): @responses.activate -def test_create_dashboard(data_config): - UID = 1 - ID = 1 +def test_create_dashboard(mock_grafana): + UID = "1" VERSION = 1 - TITLE = 'testdashboard' - dashboard = {'id': ID, 'uid': UID, 'title': TITLE, 'version': VERSION} - request = TokenRequest(**data_config, token='test') + TITLE = "testdashboard" + dashboard = {"uid": UID, "title": TITLE, "version": VERSION} + request = mock_grafana.request + assert not mock_grafana.dashboards - responses.add( - method=responses.GET, - url=request.BASE_URL + f'api/dashboards/uid/{UID}', - json={'dashboard': dashboard}) + provision.create_dashboard(request, dashboard) + assert UID in mock_grafana.dashboards - responses.add_callback( - method=responses.GET, - url=request.BASE_URL + 'api/search', - callback=lambda f: (400, {}, '')) - def post_callback(request): - body = json.loads(request.body) - return 200, {}, json.dumps(body['dashboard']) +@responses.activate +def test_create_dashboard_updates_with_existing_uid(mock_grafana): + dashboard = {'title': 'testdashboard', 'version': 1} + dash = mock_grafana.create_dashboard(dashboard) + request = mock_grafana.request - responses.add_callback( - method=responses.POST, - url=request.BASE_URL + 'api/dashboards/db', - callback=post_callback) + dashboard.update(**dash) + dashboard['data'] = 'data' + + provision.create_dashboard(request, dashboard) + in_grafana = mock_grafana.dashboards[dashboard['uid']] + assert in_grafana['data'] == 'data' + assert in_grafana['id'] == dashboard['id'] + + +@responses.activate +def test_create_dashboard_no_uid_does_not_send_id(mock_grafana): + ID = 1042 + VERSION = 1 + TITLE = 'testdashboard' + dashboard = {'id': ID, 'title': TITLE, 'version': VERSION} + request = mock_grafana.request data = provision.create_dashboard(request, dashboard) - assert data == dashboard + assert data['id'] != ID @responses.activate -def test_create_dashboard_no_uid_error(data_config): - ID = 1 +def test_create_dashboard_error(data_config): + ID = 1042 VERSION = 1 TITLE = 'testdashboard' dashboard = {'id': ID, 'title': TITLE, 'version': VERSION} @@ -189,13 +140,7 @@ def test_create_dashboard_no_uid_error(data_config): callback=lambda f: (400, {}, '')) def post_callback(request): - body = json.loads(request.body) - # if a dashboard doesn't have an UID, the ID should not be sent to - # grafana. - assert 'id' not in body['dashboard'] - - # have already tested a successful response, respond with error here. - return 400, {}, '{}' + return 400, {}, '' responses.add_callback( method=responses.POST, diff --git a/test/test_grafana_datasource.py b/test/test_grafana_datasource.py index fe93995..858c630 100644 --- a/test/test_grafana_datasource.py +++ b/test/test_grafana_datasource.py @@ -5,35 +5,23 @@ from brian_dashboard_manager.grafana.utils.request import AdminRequest @responses.activate -def test_get_datasources(data_config): - - BODY = [] - - request = AdminRequest(**data_config) - - responses.add( - method=responses.GET, - url=request.BASE_URL + 'api/datasources', json=BODY) +def test_get_datasources(mock_grafana): + mock_grafana.create_datasource({"some": "data"}) + request = mock_grafana.request data = datasource.get_datasources(request) - assert data == BODY + assert data[0]["some"] == "data" @responses.activate -def test_get_missing_datasource_definitions(data_config): - # this only retrieves data from the filesystem and checks against - # what's configured in grafana.. just make sure - # we cover the part for fetching datasources +def test_get_missing_datasource_definitions(mock_grafana, tmp_path): + request = mock_grafana.request - request = AdminRequest(**data_config) - - responses.add(method=responses.GET, url=request.BASE_URL + - 'api/datasources', json={}) - - dir = '/tmp/dirthatreallyshouldnotexistsousealonganduniquestring' - # it returns a generator, so iterate :) - for data in datasource.get_missing_datasource_definitions(request, dir): - pass + source = {"some": "data"} + (tmp_path / "datasource.json").write_text(json.dumps(source)) + assert list( + datasource.get_missing_datasource_definitions(request, str(tmp_path)) + ) == [source] def test_datasource_provisioned(): @@ -61,9 +49,7 @@ def test_datasource_provisioned(): @responses.activate -def test_create_prod_datasource(data_config): - ORG_ID = 1 - +def test_create_prod_datasource(mock_grafana): BODY = { "name": "brian-influx-datasource", "type": "influxdb", @@ -75,43 +61,12 @@ def test_create_prod_datasource(data_config): "readOnly": False } - request = AdminRequest(**data_config) - - def post_callback(request): - body = json.loads(request.body) - result = { - 'datasource': { - 'id': 1, - 'orgId': ORG_ID, - 'type': 'graphite', - 'typeLogoUrl': '', - 'password': '', - 'user': '', - 'basicAuthUser': '', - 'basicAuthPassword': '', - 'withCredentials': False, - 'jsonData': {}, - 'secureJsonFields': {}, - 'version': 1 - }, - 'id': 1, - 'message': 'Datasource added', - 'name': body['name'] - } - result['datasource'].update(body) - return 200, {}, json.dumps(result) - - responses.add_callback( - method=responses.POST, - url=request.BASE_URL + 'api/datasources', - callback=post_callback) + request = mock_grafana.request - data = provision.create_datasource( - request, BODY) + assert not mock_grafana.datasources - datasource_type = data['datasource']['type'] - datasource_config_url = data_config['datasources'][datasource_type]['url'] - assert data['datasource']['url'] != datasource_config_url + result = provision.create_datasource(request, BODY) + assert result["uid"] in mock_grafana.datasources @responses.activate diff --git a/test/test_grafana_folder.py b/test/test_grafana_folder.py index e9baedc..1fff9f1 100644 --- a/test/test_grafana_folder.py +++ b/test/test_grafana_folder.py @@ -1,8 +1,6 @@ -import json import responses from brian_dashboard_manager.grafana.folder import find_folder -from brian_dashboard_manager.grafana.utils.request import TokenRequest def generate_folder(data): @@ -24,26 +22,21 @@ def generate_folder(data): @responses.activate -def test_find_folder(data_config): +def test_find_folder(data_config, mock_grafana): + TITLE = "testfolder123" + request = mock_grafana.request + assert not mock_grafana.folders + folder = find_folder(request, TITLE, create=True) + assert folder["title"] == TITLE + assert folder["uid"] in mock_grafana.folders - TITLE = 'testfolder123' - - request = TokenRequest(**data_config, token='test') - - responses.add( - method=responses.GET, - url=f"http://{data_config['hostname']}/api/folders", - json=[]) - def folder_post(request): - data = json.loads(request.body) - return 200, {}, json.dumps(generate_folder(data)) - - responses.add_callback( - method=responses.POST, - url=f"http://{data_config['hostname']}/api/folders", - callback=folder_post) +@responses.activate +def test_find_folder_no_create(data_config, mock_grafana): + TITLE = 'testfolder123' + request = mock_grafana.request + assert not mock_grafana.folders - folder = find_folder(request, TITLE) - assert folder['id'] == 555 - assert folder['title'] == TITLE + folder = find_folder(request, TITLE, create=False) + assert folder is None + assert not mock_grafana.folders diff --git a/test/test_grafana_organization.py b/test/test_grafana_organization.py index c54b381..e9a6228 100644 --- a/test/test_grafana_organization.py +++ b/test/test_grafana_organization.py @@ -1,111 +1,51 @@ -import json import responses -from datetime import datetime, timedelta from brian_dashboard_manager.grafana import provision -from brian_dashboard_manager.grafana.utils.request import AdminRequest @responses.activate -def test_get_organizations(data_config): - request = AdminRequest(**data_config) +def test_get_organizations(mock_grafana): + request = mock_grafana.request - responses.add(method=responses.GET, - url=request.BASE_URL + 'api/orgs', - json=[{'id': 91, - 'name': 'Testorg1'}, - {'id': 92, - 'name': 'GÉANT Testorg2'}, - {'id': 93, - 'name': 'NRENsTestorg3'}, - {'id': 94, - 'name': 'General Public'}]) + mock_grafana.create_organization({"id": 91, "name": "Testorg1"}) + mock_grafana.create_organization({"id": 91, "name": "Testorg2"}) data = provision.get_organizations(request) - assert data is not None + assert [o["name"] for o in data] == ["Testorg1", "Testorg2"] @responses.activate -def test_create_organization(data_config): +def test_create_organization(mock_grafana): ORG_NAME = 'fakeorg123' + request = mock_grafana.request - def post_callback(request): - body = json.loads(request.body) - assert body['name'] == ORG_NAME - return 200, {}, json.dumps( - {'orgId': 1, 'message': 'Organization created'}) - - request = AdminRequest(**data_config) - - responses.add_callback( - method=responses.POST, - url=request.BASE_URL + 'api/orgs', - callback=post_callback) + assert not len(mock_grafana.organizations) data = provision.create_organization(request, ORG_NAME) - assert data is not None + assert len(mock_grafana.organizations) == 1 + assert next(iter(mock_grafana.organizations.values()))["id"] == data["id"] + assert data["name"] == ORG_NAME @responses.activate -def test_delete_expired_api_tokens(data_config): - ORG_ID = 1 - KEY_ID = 1 - - def post_callback(request): - assert request.params['includeExpired'] == 'True' - time = (datetime.now() - timedelta(seconds=60) - ).strftime('%Y-%m-%dT%H:%M:%SZ') - return 200, {}, json.dumps([{'expiration': time, 'id': KEY_ID}]) - - request = AdminRequest(**data_config) - responses.add_callback( - method=responses.GET, - url=request.BASE_URL + 'api/auth/keys', - callback=post_callback) - - responses.add( - method=responses.POST, - url=request.BASE_URL + - f'api/user/using/{ORG_ID}', - json={ - "message": "Active organization changed"}) - responses.add( - method=responses.DELETE, - url=request.BASE_URL + - f'api/auth/keys/{KEY_ID}', - json={ - "message": "API key deleted"}) +def test_delete_expired_api_tokens(mock_grafana): + mock_grafana.create_api_token({"secondsToLive": -1}) # an expired token + request = mock_grafana.request + assert len(mock_grafana.api_tokens) == 1 provision.delete_expired_api_tokens(request) + assert not mock_grafana.api_tokens @responses.activate -def test_create_api_token(data_config): +def test_create_api_token(mock_grafana): ORG_ID = 1 - TOKEN_ID = 1 BODY = { 'name': 'test-token', 'role': 'Admin', 'secondsToLive': 3600 } - - request = AdminRequest(**data_config) - - def post_callback(request): - body = json.loads(request.body) - assert body == BODY - return 200, {}, json.dumps({'id': TOKEN_ID}) - - responses.add_callback( - method=responses.POST, - url=request.BASE_URL + 'api/auth/keys', - callback=post_callback) - - responses.add( - method=responses.POST, - url=request.BASE_URL + - f'api/user/using/{ORG_ID}', - json={ - "message": "Active organization changed"}) - - data = provision.create_api_token(request, ORG_ID, BODY) - assert data['id'] == TOKEN_ID + request = mock_grafana.request + assert not mock_grafana.api_tokens + provision.create_api_token(request, ORG_ID, BODY) + assert len(mock_grafana.api_tokens) == 1 + assert next(iter(mock_grafana.api_tokens.values()))["name"] == "test-token" diff --git a/test/test_update.py b/test/test_update.py index 93b5bad..88ce867 100644 --- a/test/test_update.py +++ b/test/test_update.py @@ -1,9 +1,7 @@ import pytest import responses -import json -from brian_dashboard_manager.grafana.provision import provision_folder, \ - provision +from brian_dashboard_manager.grafana.provision import provision_folder, provision from test.conftest import get_test_data TEST_INTERFACES = [ @@ -623,7 +621,7 @@ def generate_folder(data): ], ) def test_provision_nren_folder( - folder_name, excluded_nrens, expected_nrens, data_config, mocker + folder_name, excluded_nrens, expected_nrens, data_config, mock_grafana ): dashboards = { "NREN": { @@ -647,35 +645,8 @@ def test_provision_nren_folder( json=get_test_data("services.json"), ) - # just return a generated folder - mocker.patch( - "brian_dashboard_manager.grafana.provision.find_folder", - return_value=generate_folder({"uid": "testfolderuid", "title": "testfolder"}), - ) - - def create_dashboard(request, dashboard, folder_id=None): - return dashboard - - mocker.patch( - "brian_dashboard_manager.grafana.provision.create_dashboard", create_dashboard - ) - - def _search_dashboard(request, dashboard, folder_id=None): - return None - - mocker.patch( - "brian_dashboard_manager.grafana.dashboard._search_dashboard", _search_dashboard - ) - - def delete_dashboard(request, dashboard, folder_id=None): - return True - - mocker.patch( - "brian_dashboard_manager.grafana.dashboard.delete_dashboard", delete_dashboard - ) - result = provision_folder( - None, + mock_grafana.request, folder_name, dashboards["NREN"], data_config, @@ -692,7 +663,7 @@ def test_provision_nren_folder( @responses.activate -def test_provision(data_config, mocker, client): +def test_provision(data_config, mocker, mock_grafana): responses.add( method=responses.GET, @@ -714,42 +685,8 @@ def test_provision(data_config, mocker, client): url=f'{data_config["inventory_provider"]}/poller/eumetsat-multicast', json=EUMETSAT_MULTICAST) - responses.add( - method=responses.DELETE, - url=f"http://{data_config['hostname']}/api/folders", - json={"message": "Deleted folder"}) - - responses.add( - method=responses.GET, - url=f"http://{data_config['hostname']}/api/folders", - json=[ - generate_folder({'uid': 'fakeuid', 'title': 'fakefolder'})]) - - def folder_post(request): - data = json.loads(request.body) - return 200, {}, json.dumps(generate_folder(data)) - - responses.add_callback( - method=responses.POST, - url=f"http://{data_config['hostname']}/api/folders", - callback=folder_post) - - def search_responses(request): - if request.params.get('query', None) == 'Home': - return 200, {}, json.dumps([]) - if request.params.get('type', None) == 'dash-db': - return 200, {}, json.dumps([]) - assert False # no other queries expected - - responses.add_callback( - method=responses.GET, - url=f"http://{data_config['hostname']}/api/search", - callback=search_responses) - - responses.add( - method=responses.GET, - url=f"http://{data_config['hostname']}/api/datasources", - json=[{ + mock_grafana.create_datasource( + { "name": "brian-influx-datasource", "type": "influxdb", "access": "proxy", @@ -757,60 +694,12 @@ def test_provision(data_config, mocker, client): "database": "test-db", "basicAuth": False, "isDefault": True, - "readOnly": False - }]) - - responses.add( - method=responses.POST, - url=f"http://{data_config['hostname']}/api/dashboards/db", - json={'uid': '999', 'id': 666}) - - responses.add( - method=responses.PUT, - url=f"http://{data_config['hostname']}/api/org/preferences", - json={'message': 'Preferences updated'}) - - def homedashboard(request): - return 404, {}, '' - - responses.add_callback( - method=responses.GET, - url=f"http://{data_config['hostname']}/api/dashboards/uid/home", - callback=homedashboard) - - PROVISIONED_ORGANIZATION = { - 'name': data_config['organizations'][0], - 'id': 0 - } - - EXISTING_ORGS = [{**org, 'id': i + 1} - for i, org in enumerate(data_config['organizations'][1:])] - - _mocked_get_organizations = mocker.patch( - 'brian_dashboard_manager.grafana.provision.get_organizations') - # all organizations are provisioned except the first one. - _mocked_get_organizations.return_value = EXISTING_ORGS.copy() - - _mocked_create_organization = mocker.patch( - 'brian_dashboard_manager.grafana.provision.create_organization') - - # spoof creating first organization - _mocked_create_organization.return_value = PROVISIONED_ORGANIZATION - - _mocked_delete_expired_api_tokens = mocker.patch( - 'brian_dashboard_manager.grafana.provision.delete_expired_api_tokens') - # we dont care about this, , tested separately - _mocked_delete_expired_api_tokens.return_value = None - - _mocked_create_api_token = mocker.patch( - 'brian_dashboard_manager.grafana.provision.create_api_token') - _mocked_create_api_token.return_value = { - 'key': 'testtoken', 'id': 0} # api token + "readOnly": False, + } + ) - _mocked_create_datasource = mocker.patch( - 'brian_dashboard_manager.grafana.provision.create_datasource') - # we dont care about this, just mark it created - _mocked_create_datasource.return_value = True + for org in data_config["organizations"][1:]: + mock_grafana.create_organization(org) _mocked_get_dashboard_definitions = mocker.patch( 'brian_dashboard_manager.grafana.provision.get_dashboard_definitions') @@ -832,14 +721,4 @@ def test_provision(data_config, mocker, client): dashboard # test dashboard ] - _mocked_create_dashboard = mocker.patch( - 'brian_dashboard_manager.grafana.provision.create_dashboard') - # we dont care about this, just mark it created - # we dont care about this, tested separately - _mocked_create_dashboard.return_value = {'uid': '999', 'id': 666} - - _mocked_delete_api_token = mocker.patch( - 'brian_dashboard_manager.grafana.provision.delete_api_token') - # we dont care about this, tested separately - _mocked_delete_api_token.return_value = None - provision(data_config) + provision(data_config, raise_exceptions=True) diff --git a/tox.ini b/tox.ini index dc78bc4..6b749b9 100644 --- a/tox.ini +++ b/tox.ini @@ -10,13 +10,12 @@ concurrency = multiprocessing,thread [testenv] deps = - pytest-xdist pytest-cov flake8 -r requirements.txt commands = coverage erase - pytest -n auto --cov brian_dashboard_manager --cov-fail-under=80 --cov-report html --cov-report xml --cov-report term -p no:checkdocs + pytest --cov brian_dashboard_manager --cov-fail-under=80 --cov-report html --cov-report xml --cov-report term -p no:checkdocs flake8 sphinx-build -M html docs/source docs/build -- GitLab