From 2281d3265423d01e60f8cc088de5a5730bb8a910 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sat, 15 May 2021 13:09:06 +0200 Subject: api: Migrate to Flask Restful This change updates the API to use Flask Restful instead of our own in-house REST library. This change reduces the maintenance effort and allows us to drastically simplify the API implementation needed for the OpenDC v2 API. --- .../opendc-web-api/tests/api/test_portfolios.py | 324 +++++++++++++++++++++ .../opendc-web-api/tests/api/test_prefabs.py | 252 ++++++++++++++++ .../opendc-web-api/tests/api/test_projects.py | 167 +++++++++++ .../opendc-web-api/tests/api/test_scenarios.py | 135 +++++++++ .../opendc-web-api/tests/api/test_schedulers.py | 22 ++ .../opendc-web-api/tests/api/test_topologies.py | 140 +++++++++ opendc-web/opendc-web-api/tests/api/test_traces.py | 40 +++ 7 files changed, 1080 insertions(+) create mode 100644 opendc-web/opendc-web-api/tests/api/test_portfolios.py create mode 100644 opendc-web/opendc-web-api/tests/api/test_prefabs.py create mode 100644 opendc-web/opendc-web-api/tests/api/test_projects.py create mode 100644 opendc-web/opendc-web-api/tests/api/test_scenarios.py create mode 100644 opendc-web/opendc-web-api/tests/api/test_schedulers.py create mode 100644 opendc-web/opendc-web-api/tests/api/test_topologies.py create mode 100644 opendc-web/opendc-web-api/tests/api/test_traces.py (limited to 'opendc-web/opendc-web-api/tests/api') diff --git a/opendc-web/opendc-web-api/tests/api/test_portfolios.py b/opendc-web/opendc-web-api/tests/api/test_portfolios.py new file mode 100644 index 00000000..da7991f6 --- /dev/null +++ b/opendc-web/opendc-web-api/tests/api/test_portfolios.py @@ -0,0 +1,324 @@ +# Copyright (c) 2021 AtLarge Research +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from opendc.exts import db + +test_id = 24 * '1' +test_id_2 = 24 * '2' + + +def test_get_portfolio_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.get(f'/portfolios/{test_id}').status + + +def test_get_portfolio_no_authorizations(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={'projectId': test_id, 'authorizations': []}) + res = client.get(f'/portfolios/{test_id}') + assert '403' in res.status + + +def test_get_portfolio_not_authorized(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + 'projectId': test_id, + '_id': test_id, + 'authorizations': [] + }) + res = client.get(f'/portfolios/{test_id}') + assert '403' in res.status + + +def test_get_portfolio(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + 'projectId': test_id, + '_id': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'EDIT' + }] + }) + res = client.get(f'/portfolios/{test_id}') + assert '200' in res.status + + +def test_update_portfolio_missing_parameter(client): + assert '400' in client.put(f'/portfolios/{test_id}').status + + +def test_update_portfolio_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.put(f'/portfolios/{test_id}', json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_update_portfolio_not_authorized(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'VIEW' + }] + }) + mocker.patch.object(db, 'update', return_value={}) + assert '403' in client.put(f'/portfolios/{test_id}', json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_update_portfolio(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }], + 'targets': { + 'enabledMetrics': [], + 'repeatsPerScenario': 1 + } + }) + mocker.patch.object(db, 'update', return_value={}) + + res = client.put(f'/portfolios/{test_id}', json={'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + }}) + assert '200' in res.status + + +def test_delete_project_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.delete(f'/portfolios/{test_id}').status + + +def test_delete_project_different_user(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'googleId': 'other_test', + 'authorizations': [{ + 'userId': 'test', + 'level': 'VIEW' + }] + }) + mocker.patch.object(db, 'delete_one', return_value=None) + assert '403' in client.delete(f'/portfolios/{test_id}').status + + +def test_delete_project(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'googleId': 'test', + 'portfolioIds': [test_id], + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }] + }) + mocker.patch.object(db, 'delete_one', return_value={}) + mocker.patch.object(db, 'update', return_value=None) + res = client.delete(f'/portfolios/{test_id}') + assert '200' in res.status + + +def test_add_topology_missing_parameter(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'googleId': 'test', + 'portfolioIds': [test_id], + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }] + }) + assert '400' in client.post(f'/projects/{test_id}/topologies').status + + +def test_add_topology(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }], + 'topologyIds': [] + }) + mocker.patch.object(db, + 'insert', + return_value={ + '_id': test_id, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'topologyIds': [] + }) + mocker.patch.object(db, 'update', return_value={}) + res = client.post(f'/projects/{test_id}/topologies', json={'topology': {'name': 'test project', 'rooms': []}}) + assert 'rooms' in res.json['data'] + assert '200' in res.status + + +def test_add_topology_not_authorized(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'VIEW' + }] + }) + assert '403' in client.post(f'/projects/{test_id}/topologies', + json={ + 'topology': { + 'name': 'test_topology', + 'rooms': [] + } + }).status + + +def test_add_portfolio_missing_parameter(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'googleId': 'test', + 'portfolioIds': [test_id], + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }] + }) + assert '400' in client.post(f'/projects/{test_id}/portfolios').status + + +def test_add_portfolio_non_existing_project(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.post(f'/projects/{test_id}/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_add_portfolio_not_authorized(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'VIEW' + }] + }) + assert '403' in client.post(f'/projects/{test_id}/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }).status + + +def test_add_portfolio(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'portfolioIds': [test_id], + 'authorizations': [{ + 'userId': 'test', + 'level': 'EDIT' + }] + }) + mocker.patch.object(db, + 'insert', + return_value={ + '_id': test_id, + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + }, + 'projectId': test_id, + 'scenarioIds': [], + }) + mocker.patch.object(db, 'update', return_value=None) + res = client.post( + f'/projects/{test_id}/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }) + assert 'projectId' in res.json['data'] + assert 'scenarioIds' in res.json['data'] + assert '200' in res.status diff --git a/opendc-web/opendc-web-api/tests/api/test_prefabs.py b/opendc-web/opendc-web-api/tests/api/test_prefabs.py new file mode 100644 index 00000000..ea3d92d6 --- /dev/null +++ b/opendc-web/opendc-web-api/tests/api/test_prefabs.py @@ -0,0 +1,252 @@ +# Copyright (c) 2021 AtLarge Research +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from unittest.mock import Mock +from opendc.exts import db + +test_id = 24 * '1' +test_id_2 = 24 * '2' + + +def test_add_prefab_missing_parameter(client): + assert '400' in client.post('/prefabs/').status + + +def test_add_prefab(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id, 'authorizations': []}) + mocker.patch.object(db, + 'insert', + return_value={ + '_id': test_id, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': test_id + }) + res = client.post('/prefabs/', json={'prefab': {'name': 'test prefab'}}) + assert 'datetimeCreated' in res.json['data'] + assert 'datetimeLastEdited' in res.json['data'] + assert 'authorId' in res.json['data'] + assert '200' in res.status + + +def test_get_prefabs(client, mocker): + db.fetch_all = Mock() + mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id}) + db.fetch_all.side_effect = [ + [{ + '_id': test_id, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': test_id, + 'visibility' : 'private' + }, + { + '_id': '2' * 24, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': test_id, + 'visibility' : 'private' + }, + { + '_id': '3' * 24, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': test_id, + 'visibility' : 'public' + }, + { + '_id': '4' * 24, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': test_id, + 'visibility' : 'public' + }], + [{ + '_id': '5' * 24, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': '2' * 24, + 'visibility' : 'public' + }, + { + '_id': '6' * 24, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': '2' * 24, + 'visibility' : 'public' + }, + { + '_id': '7' * 24, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': '2' * 24, + 'visibility' : 'public' + }, + { + '_id': '8' * 24, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'authorId': '2' * 24, + 'visibility' : 'public' + }] + ] + mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id}) + res = client.get('/prefabs/') + assert '200' in res.status + + +def test_get_prefab_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.get(f'/prefabs/{test_id}').status + + +def test_get_private_prefab_not_authorized(client, mocker): + db.fetch_one = Mock() + db.fetch_one.side_effect = [{ + '_id': test_id, + 'name': 'test prefab', + 'authorId': test_id_2, + 'visibility': 'private', + 'rack': {} + }, + { + '_id': test_id + } + ] + res = client.get(f'/prefabs/{test_id}') + assert '403' in res.status + + +def test_get_private_prefab(client, mocker): + db.fetch_one = Mock() + db.fetch_one.side_effect = [{ + '_id': test_id, + 'name': 'test prefab', + 'authorId': 'test', + 'visibility': 'private', + 'rack': {} + }, + { + '_id': test_id + } + ] + res = client.get(f'/prefabs/{test_id}') + assert '200' in res.status + + +def test_get_public_prefab(client, mocker): + db.fetch_one = Mock() + db.fetch_one.side_effect = [{ + '_id': test_id, + 'name': 'test prefab', + 'authorId': test_id_2, + 'visibility': 'public', + 'rack': {} + }, + { + '_id': test_id + } + ] + res = client.get(f'/prefabs/{test_id}') + assert '200' in res.status + + +def test_update_prefab_missing_parameter(client): + assert '400' in client.put(f'/prefabs/{test_id}').status + + +def test_update_prefab_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.put(f'/prefabs/{test_id}', json={'prefab': {'name': 'S'}}).status + + +def test_update_prefab_not_authorized(client, mocker): + db.fetch_one = Mock() + db.fetch_one.side_effect = [{ + '_id': test_id, + 'name': 'test prefab', + 'authorId': test_id_2, + 'visibility': 'private', + 'rack': {} + }, + { + '_id': test_id + } + ] + mocker.patch.object(db, 'update', return_value={}) + assert '403' in client.put(f'/prefabs/{test_id}', json={'prefab': {'name': 'test prefab', 'rack': {}}}).status + + +def test_update_prefab(client, mocker): + db.fetch_one = Mock() + db.fetch_one.side_effect = [{ + '_id': test_id, + 'name': 'test prefab', + 'authorId': 'test', + 'visibility': 'private', + 'rack': {} + }, + { + '_id': test_id + } + ] + mocker.patch.object(db, 'update', return_value={}) + res = client.put(f'/prefabs/{test_id}', json={'prefab': {'name': 'test prefab', 'rack': {}}}) + assert '200' in res.status + + +def test_delete_prefab_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.delete(f'/prefabs/{test_id}').status + + +def test_delete_prefab_different_user(client, mocker): + db.fetch_one = Mock() + db.fetch_one.side_effect = [{ + '_id': test_id, + 'name': 'test prefab', + 'authorId': test_id_2, + 'visibility': 'private', + 'rack': {} + }, + { + '_id': test_id + } + ] + mocker.patch.object(db, 'delete_one', return_value=None) + assert '403' in client.delete(f'/prefabs/{test_id}').status + + +def test_delete_prefab(client, mocker): + db.fetch_one = Mock() + db.fetch_one.side_effect = [{ + '_id': test_id, + 'name': 'test prefab', + 'authorId': 'test', + 'visibility': 'private', + 'rack': {} + }, + { + '_id': test_id + } + ] + mocker.patch.object(db, 'delete_one', return_value={'prefab': {'name': 'name'}}) + res = client.delete(f'/prefabs/{test_id}') + assert '200' in res.status diff --git a/opendc-web/opendc-web-api/tests/api/test_projects.py b/opendc-web/opendc-web-api/tests/api/test_projects.py new file mode 100644 index 00000000..c4c82e0d --- /dev/null +++ b/opendc-web/opendc-web-api/tests/api/test_projects.py @@ -0,0 +1,167 @@ +# Copyright (c) 2021 AtLarge Research +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from opendc.exts import db + +test_id = 24 * '1' + + +def test_get_user_projects(client, mocker): + mocker.patch.object(db, 'fetch_all', return_value={'_id': test_id, 'authorizations': [{'userId': 'test', + 'level': 'OWN'}]}) + res = client.get('/projects/') + assert '200' in res.status + + +def test_add_project_missing_parameter(client): + assert '400' in client.post('/projects/').status + + +def test_add_project(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id, 'authorizations': []}) + mocker.patch.object(db, + 'insert', + return_value={ + '_id': test_id, + 'datetimeCreated': '000', + 'datetimeLastEdited': '000', + 'topologyIds': [] + }) + mocker.patch.object(db, 'update', return_value={}) + res = client.post('/projects/', json={'project': {'name': 'test project'}}) + assert 'datetimeCreated' in res.json['data'] + assert 'datetimeLastEdited' in res.json['data'] + assert 'topologyIds' in res.json['data'] + assert '200' in res.status + + +def test_get_project_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.get(f'/projects/{test_id}').status + + +def test_get_project_no_authorizations(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={'authorizations': []}) + res = client.get(f'/projects/{test_id}') + assert '403' in res.status + + +def test_get_project_not_authorized(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'authorizations': [] + }) + res = client.get(f'/projects/{test_id}') + assert '403' in res.status + + +def test_get_project(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'EDIT' + }] + }) + res = client.get(f'/projects/{test_id}') + assert '200' in res.status + + +def test_update_project_missing_parameter(client): + assert '400' in client.put(f'/projects/{test_id}').status + + +def test_update_project_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.put(f'/projects/{test_id}', json={'project': {'name': 'S'}}).status + + +def test_update_project_not_authorized(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'VIEW' + }] + }) + mocker.patch.object(db, 'update', return_value={}) + assert '403' in client.put(f'/projects/{test_id}', json={'project': {'name': 'S'}}).status + + +def test_update_project(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }] + }) + mocker.patch.object(db, 'update', return_value={}) + + res = client.put(f'/projects/{test_id}', json={'project': {'name': 'S'}}) + assert '200' in res.status + + +def test_delete_project_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.delete(f'/projects/{test_id}').status + + +def test_delete_project_different_user(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'googleId': 'other_test', + 'authorizations': [{ + 'userId': 'test', + 'level': 'VIEW' + }], + 'topologyIds': [] + }) + mocker.patch.object(db, 'delete_one', return_value=None) + assert '403' in client.delete(f'/projects/{test_id}').status + + +def test_delete_project(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'googleId': 'test', + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }], + 'topologyIds': [], + 'portfolioIds': [], + }) + mocker.patch.object(db, 'update', return_value=None) + mocker.patch.object(db, 'delete_one', return_value={'googleId': 'test'}) + res = client.delete(f'/projects/{test_id}') + assert '200' in res.status diff --git a/opendc-web/opendc-web-api/tests/api/test_scenarios.py b/opendc-web/opendc-web-api/tests/api/test_scenarios.py new file mode 100644 index 00000000..bdd5c4a3 --- /dev/null +++ b/opendc-web/opendc-web-api/tests/api/test_scenarios.py @@ -0,0 +1,135 @@ +# Copyright (c) 2021 AtLarge Research +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from opendc.exts import db + +test_id = 24 * '1' +test_id_2 = 24 * '2' + + +def test_get_scenario_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.get(f'/scenarios/{test_id}').status + + +def test_get_scenario_no_authorizations(client, mocker): + m = mocker.MagicMock() + m.side_effect = ({'portfolioId': test_id}, {'projectId': test_id}, {'authorizations': []}) + mocker.patch.object(db, 'fetch_one', m) + res = client.get(f'/scenarios/{test_id}') + assert '403' in res.status + + +def test_get_scenario(client, mocker): + mocker.patch.object(db, + 'fetch_one', + side_effect=[ + {'portfolioId': test_id}, + {'projectId': test_id}, + {'authorizations': + [{'userId': 'test', 'level': 'OWN'}] + }]) + res = client.get(f'/scenarios/{test_id}') + assert '200' in res.status + + +def test_update_scenario_missing_parameter(client): + assert '400' in client.put(f'/scenarios/{test_id}').status + + +def test_update_scenario_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.put(f'/scenarios/{test_id}', json={ + 'scenario': { + 'name': 'test', + } + }).status + + +def test_update_scenario_not_authorized(client, mocker): + mocker.patch.object(db, + 'fetch_one', + side_effect=[ + {'portfolioId': test_id}, + {'projectId': test_id}, + {'authorizations': + [{'userId': 'test', 'level': 'VIEW'}] + }]) + mocker.patch.object(db, 'update', return_value={}) + assert '403' in client.put(f'/scenarios/{test_id}', json={ + 'scenario': { + 'name': 'test', + } + }).status + + +def test_update_scenario(client, mocker): + mocker.patch.object(db, + 'fetch_one', + side_effect=[ + {'_id': test_id, 'portfolioId': test_id}, + {'projectId': test_id}, + {'authorizations': + [{'userId': 'test', 'level': 'OWN'}] + }]) + mocker.patch.object(db, 'update', return_value={}) + + res = client.put(f'/scenarios/{test_id}', json={'scenario': { + 'name': 'test', + }}) + assert '200' in res.status + + +def test_delete_project_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.delete(f'/scenarios/{test_id}').status + + +def test_delete_project_different_user(client, mocker): + mocker.patch.object(db, + 'fetch_one', + side_effect=[ + {'_id': test_id, 'portfolioId': test_id}, + {'projectId': test_id}, + {'authorizations': + [{'userId': 'test', 'level': 'VIEW'}] + }]) + mocker.patch.object(db, 'delete_one', return_value=None) + assert '403' in client.delete(f'/scenarios/{test_id}').status + + +def test_delete_project(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'portfolioId': test_id, + 'googleId': 'test', + 'scenarioIds': [test_id], + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }] + }) + mocker.patch.object(db, 'delete_one', return_value={}) + mocker.patch.object(db, 'update', return_value=None) + res = client.delete(f'/scenarios/{test_id}') + assert '200' in res.status diff --git a/opendc-web/opendc-web-api/tests/api/test_schedulers.py b/opendc-web/opendc-web-api/tests/api/test_schedulers.py new file mode 100644 index 00000000..5d9e6995 --- /dev/null +++ b/opendc-web/opendc-web-api/tests/api/test_schedulers.py @@ -0,0 +1,22 @@ +# Copyright (c) 2021 AtLarge Research +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +def test_get_schedulers(client): + assert '200' in client.get('/schedulers/').status diff --git a/opendc-web/opendc-web-api/tests/api/test_topologies.py b/opendc-web/opendc-web-api/tests/api/test_topologies.py new file mode 100644 index 00000000..6e7c54ef --- /dev/null +++ b/opendc-web/opendc-web-api/tests/api/test_topologies.py @@ -0,0 +1,140 @@ +# Copyright (c) 2021 AtLarge Research +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from opendc.exts import db + +test_id = 24 * '1' +test_id_2 = 24 * '2' + + +def test_get_topology(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'EDIT' + }] + }) + res = client.get(f'/topologies/{test_id}') + assert '200' in res.status + + +def test_get_topology_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.get('/topologies/1').status + + +def test_get_topology_not_authorized(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'authorizations': [] + }) + res = client.get(f'/topologies/{test_id}') + assert '403' in res.status + + +def test_get_topology_no_authorizations(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={'projectId': test_id, 'authorizations': []}) + res = client.get(f'/topologies/{test_id}') + assert '403' in res.status + + +def test_update_topology_missing_parameter(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'authorizations': [] + }) + assert '400' in client.put(f'/topologies/{test_id}').status + + +def test_update_topology_non_existent(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.put(f'/topologies/{test_id}', json={'topology': {'name': 'test_topology', 'rooms': []}}).status + + +def test_update_topology_not_authorized(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'authorizations': [] + }) + mocker.patch.object(db, 'update', return_value={}) + assert '403' in client.put(f'/topologies/{test_id}', json={ + 'topology': { + 'name': 'updated_topology', + 'rooms': [] + } + }).status + + +def test_update_topology(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }] + }) + mocker.patch.object(db, 'update', return_value={}) + + assert '200' in client.put(f'/topologies/{test_id}', json={ + 'topology': { + 'name': 'updated_topology', + 'rooms': [] + } + }).status + + +def test_delete_topology(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'projectId': test_id, + 'googleId': 'test', + 'topologyIds': [test_id], + 'authorizations': [{ + 'userId': 'test', + 'level': 'OWN' + }] + }) + mocker.patch.object(db, 'delete_one', return_value={}) + mocker.patch.object(db, 'update', return_value=None) + res = client.delete(f'/topologies/{test_id}') + assert '200' in res.status + + +def test_delete_nonexistent_topology(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.delete(f'/topologies/{test_id}').status diff --git a/opendc-web/opendc-web-api/tests/api/test_traces.py b/opendc-web/opendc-web-api/tests/api/test_traces.py new file mode 100644 index 00000000..0b252c2f --- /dev/null +++ b/opendc-web/opendc-web-api/tests/api/test_traces.py @@ -0,0 +1,40 @@ +# Copyright (c) 2021 AtLarge Research +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from opendc.exts import db + +test_id = 24 * '1' + + +def test_get_traces(client, mocker): + mocker.patch.object(db, 'fetch_all', return_value=[]) + assert '200' in client.get('/traces/').status + + +def test_get_trace_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.get(f'/traces/{test_id}').status + + +def test_get_trace(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={'name': 'test trace'}) + res = client.get(f'/traces/{test_id}') + assert 'name' in res.json['data'] + assert '200' in res.status -- cgit v1.2.3 From a2a5979bfb392565b55e489b6020aa391e782eb0 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 2 Jul 2021 16:14:52 +0200 Subject: api: Add endpoint for simulation jobs This change adds an API endpoint for simulation jobs which allows API consumers to manage simulation jobs without needing direct database access that is currently needed for the web runner. --- opendc-web/opendc-web-api/tests/api/test_jobs.py | 139 +++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 opendc-web/opendc-web-api/tests/api/test_jobs.py (limited to 'opendc-web/opendc-web-api/tests/api') diff --git a/opendc-web/opendc-web-api/tests/api/test_jobs.py b/opendc-web/opendc-web-api/tests/api/test_jobs.py new file mode 100644 index 00000000..2efe6933 --- /dev/null +++ b/opendc-web/opendc-web-api/tests/api/test_jobs.py @@ -0,0 +1,139 @@ +# Copyright (c) 2021 AtLarge Research +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# +from datetime import datetime + +from opendc.exts import db + +test_id = 24 * '1' +test_id_2 = 24 * '2' + + +def test_get_jobs(client, mocker): + mocker.patch.object(db, 'fetch_all', return_value=[ + {'_id': 'a', 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'}} + ]) + res = client.get('/jobs/') + assert '200' in res.status + + +def test_get_job_non_existing(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value=None) + assert '404' in client.get(f'/jobs/{test_id}').status + + +def test_get_job(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={ + '_id': 'a', 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} + }) + res = client.get(f'/jobs/{test_id}') + assert '200' in res.status + + +def test_update_job_nop(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} + }) + update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', + 'simulation': {'state': 'QUEUED', 'heartbeat': datetime.now()} + }) + res = client.post(f'/jobs/{test_id}', json={'state': 'QUEUED'}) + assert '200' in res.status + update_mock.assert_called_once() + + +def test_update_job_invalid_state(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} + }) + res = client.post(f'/jobs/{test_id}', json={'state': 'FINISHED'}) + assert '400' in res.status + + +def test_update_job_claim(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} + }) + update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', + 'simulation': {'state': 'CLAIMED', 'heartbeat': datetime.now()} + }) + res = client.post(f'/jobs/{test_id}', json={'state': 'CLAIMED'}) + assert '200' in res.status + update_mock.assert_called_once() + + +def test_update_job_conflict(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} + }) + update_mock = mocker.patch.object(db, 'fetch_and_update', return_value=None) + res = client.post(f'/jobs/{test_id}', json={'state': 'CLAIMED'}) + assert '409' in res.status + update_mock.assert_called_once() + + +def test_update_job_run(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'CLAIMED'} + }) + update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', + 'simulation': {'state': 'RUNNING', 'heartbeat': datetime.now()} + }) + res = client.post(f'/jobs/{test_id}', json={'state': 'RUNNING'}) + assert '200' in res.status + update_mock.assert_called_once() + + +def test_update_job_finished(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'RUNNING'} + }) + update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', + 'simulation': {'state': 'FINISHED', 'heartbeat': datetime.now()} + }) + res = client.post(f'/jobs/{test_id}', json={'state': 'FINISHED'}) + assert '200' in res.status + update_mock.assert_called_once() + + +def test_update_job_failed(client, mocker): + mocker.patch.object(db, 'fetch_one', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'RUNNING'} + }) + update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ + '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', + 'simulation': {'state': 'FAILED', 'heartbeat': datetime.now()} + }) + res = client.post(f'/jobs/{test_id}', json={'state': 'FAILED'}) + assert '200' in res.status + update_mock.assert_called_once() -- cgit v1.2.3 From 5ec19973eb3d23046d874b097275857a58c23082 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 20:45:06 +0200 Subject: api: Add endpoints for accessing project relations This change adds additional endpoints to the REST API to access the project relations, the portfolios and topologies that belong to a project. --- .../opendc-web-api/tests/api/test_portfolios.py | 16 ++++++++++++ .../opendc-web-api/tests/api/test_projects.py | 30 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) (limited to 'opendc-web/opendc-web-api/tests/api') diff --git a/opendc-web/opendc-web-api/tests/api/test_portfolios.py b/opendc-web/opendc-web-api/tests/api/test_portfolios.py index da7991f6..196fcb1c 100644 --- a/opendc-web/opendc-web-api/tests/api/test_portfolios.py +++ b/opendc-web/opendc-web-api/tests/api/test_portfolios.py @@ -322,3 +322,19 @@ def test_add_portfolio(client, mocker): assert 'projectId' in res.json['data'] assert 'scenarioIds' in res.json['data'] assert '200' in res.status + + +def test_get_portfolio_scenarios(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + 'projectId': test_id, + '_id': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'EDIT' + }] + }) + mocker.patch.object(db, 'fetch_all', return_value=[{'_id': test_id}]) + res = client.get(f'/portfolios/{test_id}/scenarios') + assert '200' in res.status diff --git a/opendc-web/opendc-web-api/tests/api/test_projects.py b/opendc-web/opendc-web-api/tests/api/test_projects.py index c4c82e0d..1cfe4c52 100644 --- a/opendc-web/opendc-web-api/tests/api/test_projects.py +++ b/opendc-web/opendc-web-api/tests/api/test_projects.py @@ -30,6 +30,36 @@ def test_get_user_projects(client, mocker): assert '200' in res.status +def test_get_user_topologies(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'EDIT' + }] + }) + mocker.patch.object(db, 'fetch_all', return_value=[{'_id': test_id}]) + res = client.get(f'/projects/{test_id}/topologies') + assert '200' in res.status + + +def test_get_user_portfolios(client, mocker): + mocker.patch.object(db, + 'fetch_one', + return_value={ + '_id': test_id, + 'authorizations': [{ + 'userId': 'test', + 'level': 'EDIT' + }] + }) + mocker.patch.object(db, 'fetch_all', return_value=[{'_id': test_id}]) + res = client.get(f'/projects/{test_id}/portfolios') + assert '200' in res.status + + def test_add_project_missing_parameter(client): assert '400' in client.post('/projects/').status -- cgit v1.2.3