diff options
Diffstat (limited to 'web-server')
26 files changed, 642 insertions, 217 deletions
diff --git a/web-server/main.py b/web-server/main.py index af3e95b9..c466c0f2 100644 --- a/web-server/main.py +++ b/web-server/main.py @@ -140,10 +140,8 @@ def serve_web_server_test(): @FLASK_CORE_APP.route('/') @FLASK_CORE_APP.route('/projects') @FLASK_CORE_APP.route('/projects/<path:project_id>') -@FLASK_CORE_APP.route('/projects/<path:project_id>/experiments') -@FLASK_CORE_APP.route('/projects/<path:project_id>/experiments/<path:experiment_id>') @FLASK_CORE_APP.route('/profile') -def serve_index(project_id=None, experiment_id=None): +def serve_index(project_id=None): return send_from_directory(STATIC_ROOT, 'index.html') diff --git a/web-server/opendc/api/v2/experiments/experimentId/endpoint.py b/web-server/opendc/api/v2/experiments/experimentId/endpoint.py deleted file mode 100644 index 6706dc57..00000000 --- a/web-server/opendc/api/v2/experiments/experimentId/endpoint.py +++ /dev/null @@ -1,56 +0,0 @@ -from opendc.models.experiment import Experiment -from opendc.models.project import Project -from opendc.util.rest import Response - - -def GET(request): - """Get this Experiment.""" - - request.check_required_parameters(path={'experimentId': 'string'}) - - experiment = Experiment.from_id(request.params_path['experimentId']) - - experiment.check_exists() - experiment.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved Experiment.', experiment.obj) - - -def PUT(request): - """Update this Experiments name.""" - - request.check_required_parameters(path={'experimentId': 'string'}, body={'experiment': { - 'name': 'string', - }}) - - experiment = Experiment.from_id(request.params_path['experimentId']) - - experiment.check_exists() - experiment.check_user_access(request.google_id, True) - - experiment.set_property('name', request.params_body['experiment']['name']) - - experiment.update() - - return Response(200, 'Successfully updated experiment.', experiment.obj) - - -def DELETE(request): - """Delete this Experiment.""" - - request.check_required_parameters(path={'experimentId': 'string'}) - - experiment = Experiment.from_id(request.params_path['experimentId']) - - experiment.check_exists() - experiment.check_user_access(request.google_id, True) - - project = Project.from_id(experiment.obj['projectId']) - project.check_exists() - if request.params_path['experimentId'] in project.obj['experimentIds']: - project.obj['experimentIds'].remove(request.params_path['experimentId']) - project.update() - - old_object = experiment.delete() - - return Response(200, 'Successfully deleted experiment.', old_object) diff --git a/web-server/opendc/api/v2/paths.json b/web-server/opendc/api/v2/paths.json index 01a7dcff..90d5a2e6 100644 --- a/web-server/opendc/api/v2/paths.json +++ b/web-server/opendc/api/v2/paths.json @@ -5,9 +5,11 @@ "/projects/{projectId}", "/projects/{projectId}/authorizations", "/projects/{projectId}/topologies", + "/projects/{projectId}/portfolios", "/topologies/{topologyId}", - "/projects/{projectId}/experiments", - "/experiments/{experimentId}", + "/portfolios/{portfolioId}", + "/portfolios/{portfolioId}/scenarios", + "/scenarios/{scenarioId}", "/schedulers", "/traces", "/traces/{traceId}", diff --git a/web-server/opendc/api/v2/experiments/__init__.py b/web-server/opendc/api/v2/portfolios/__init__.py index e69de29b..e69de29b 100644 --- a/web-server/opendc/api/v2/experiments/__init__.py +++ b/web-server/opendc/api/v2/portfolios/__init__.py diff --git a/web-server/opendc/api/v2/experiments/experimentId/__init__.py b/web-server/opendc/api/v2/portfolios/portfolioId/__init__.py index e69de29b..e69de29b 100644 --- a/web-server/opendc/api/v2/experiments/experimentId/__init__.py +++ b/web-server/opendc/api/v2/portfolios/portfolioId/__init__.py diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py new file mode 100644 index 00000000..c0ca64e0 --- /dev/null +++ b/web-server/opendc/api/v2/portfolios/portfolioId/endpoint.py @@ -0,0 +1,65 @@ +from opendc.models.portfolio import Portfolio +from opendc.models.project import Project +from opendc.util.rest import Response + + +def GET(request): + """Get this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, False) + + return Response(200, 'Successfully retrieved portfolio.', portfolio.obj) + + +def PUT(request): + """Update this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}, body={'portfolio': { + 'name': 'string', + 'targets': { + 'enabledMetrics': 'list', + 'repeatsPerScenario': 'int', + }, + }}) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, True) + + portfolio.set_property('name', + request.params_body['portfolio']['name']) + portfolio.set_property('targets.enabledMetrics', + request.params_body['portfolio']['targets']['enabledMetrics']) + portfolio.set_property('targets.repeatsPerScenario', + request.params_body['portfolio']['targets']['repeatsPerScenario']) + + portfolio.update() + + return Response(200, 'Successfully updated portfolio.', portfolio.obj) + + +def DELETE(request): + """Delete this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, True) + + project = Project.from_id(portfolio.obj['projectId']) + project.check_exists() + if request.params_path['portfolioId'] in project.obj['portfolioIds']: + project.obj['portfolioIds'].remove(request.params_path['portfolioId']) + project.update() + + old_object = portfolio.delete() + + return Response(200, 'Successfully deleted portfolio.', old_object) diff --git a/web-server/opendc/api/v2/projects/projectId/experiments/__init__.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py index e69de29b..e69de29b 100644 --- a/web-server/opendc/api/v2/projects/projectId/experiments/__init__.py +++ b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py new file mode 100644 index 00000000..ab32aae2 --- /dev/null +++ b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py @@ -0,0 +1,42 @@ +from opendc.models.portfolio import Portfolio +from opendc.util.rest import Response + + +def POST(request): + """Add a new Scenario for this Portfolio.""" + + request.check_required_parameters(path={'portfolioId': 'string'}, + body={ + 'scenario': { + 'name': 'string', + 'trace': { + 'traceId': 'string', + 'loadSamplingFraction': 'float', + }, + 'topology': { + 'topologyId': 'string', + }, + 'operational': { + 'failuresEnabled': 'bool', + 'performanceInterferenceEnabled': 'bool', + 'schedulerName': 'string', + }, + } + }) + + portfolio = Portfolio.from_id(request.params_path['portfolioId']) + + portfolio.check_exists() + portfolio.check_user_access(request.google_id, True) + + scenario = Portfolio(request.params_body['scenario']) + + scenario.set_property('portfolioId', request.params_path['portfolioId']) + scenario.set_property('simulationState', 'QUEUED') + + scenario.insert() + + portfolio.obj['portfolioIds'].append(portfolio.get_id()) + portfolio.update() + + return Response(200, 'Successfully added Portfolio.', portfolio.obj) diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py new file mode 100644 index 00000000..8b55bab0 --- /dev/null +++ b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py @@ -0,0 +1,119 @@ +from opendc.util.database import DB + + +def test_add_scenario_missing_parameter(client): + assert '400' in client.post('/api/v2/portfolios/1/scenarios').status + + +def test_add_scenario_non_existing_portfolio(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.post('/api/v2/portfolios/1/scenarios', + json={ + 'scenario': { + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + } + }).status + + +def test_add_scenario_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'portfolioId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + assert '403' in client.post('/api/v2/portfolios/1/scenarios', + json={ + 'scenario': { + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + } + }).status + + +def test_add_scenario(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'projectId': '1', + 'portfolioId': '1', + 'portfolioIds': ['1'], + 'scenarioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }], + 'simulationState': 'QUEUED', + }) + mocker.patch.object(DB, + 'insert', + return_value={ + '_id': '1', + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + 'portfolioId': '1', + 'simulationState': 'QUEUED', + }) + mocker.patch.object(DB, 'update', return_value=None) + res = client.post( + '/api/v2/portfolios/1/scenarios', + json={ + 'scenario': { + 'name': 'test', + 'trace': { + 'traceId': '1', + 'loadSamplingFraction': 1.0, + }, + 'topology': { + 'topologyId': '1', + }, + 'operational': { + 'failuresEnabled': True, + 'performanceInterferenceEnabled': False, + 'schedulerName': 'DEFAULT', + }, + } + }) + assert 'portfolioId' in res.json['content'] + assert 'simulationState' in res.json['content'] + assert '200' in res.status diff --git a/web-server/opendc/api/v2/experiments/experimentId/test_endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py index a284cf32..7ac346d4 100644 --- a/web-server/opendc/api/v2/experiments/experimentId/test_endpoint.py +++ b/web-server/opendc/api/v2/portfolios/portfolioId/test_endpoint.py @@ -1,18 +1,18 @@ from opendc.util.database import DB -def test_get_experiment_non_existing(client, mocker): +def test_get_portfolio_non_existing(client, mocker): mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/api/v2/experiments/1').status + assert '404' in client.get('/api/v2/portfolios/1').status -def test_get_experiment_no_authorizations(client, mocker): +def test_get_portfolio_no_authorizations(client, mocker): mocker.patch.object(DB, 'fetch_one', return_value={'projectId': '1', 'authorizations': []}) - res = client.get('/api/v2/experiments/1') + res = client.get('/api/v2/portfolios/1') assert '403' in res.status -def test_get_experiment_not_authorized(client, mocker): +def test_get_portfolio_not_authorized(client, mocker): mocker.patch.object(DB, 'fetch_one', return_value={ @@ -23,11 +23,11 @@ def test_get_experiment_not_authorized(client, mocker): 'authorizationLevel': 'OWN' }] }) - res = client.get('/api/v2/experiments/1') + res = client.get('/api/v2/portfolios/1') assert '403' in res.status -def test_get_experiment(client, mocker): +def test_get_portfolio(client, mocker): mocker.patch.object(DB, 'fetch_one', return_value={ @@ -38,24 +38,28 @@ def test_get_experiment(client, mocker): 'authorizationLevel': 'EDIT' }] }) - res = client.get('/api/v2/experiments/1') + res = client.get('/api/v2/portfolios/1') assert '200' in res.status -def test_update_experiment_missing_parameter(client): - assert '400' in client.put('/api/v2/experiments/1').status +def test_update_portfolio_missing_parameter(client): + assert '400' in client.put('/api/v2/portfolios/1').status -def test_update_experiment_non_existing(client, mocker): +def test_update_portfolio_non_existing(client, mocker): mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put('/api/v2/experiments/1', json={ - 'experiment': { + assert '404' in client.put('/api/v2/portfolios/1', json={ + 'portfolio': { 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } } }).status -def test_update_experiment_not_authorized(client, mocker): +def test_update_portfolio_not_authorized(client, mocker): mocker.patch.object(DB, 'fetch_one', return_value={ @@ -67,14 +71,18 @@ def test_update_experiment_not_authorized(client, mocker): }] }) mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put('/api/v2/experiments/1', json={ - 'experiment': { + assert '403' in client.put('/api/v2/portfolios/1', json={ + 'portfolio': { 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } } }).status -def test_update_experiment(client, mocker): +def test_update_portfolio(client, mocker): mocker.patch.object(DB, 'fetch_one', return_value={ @@ -83,19 +91,27 @@ def test_update_experiment(client, mocker): 'authorizations': [{ 'projectId': '1', 'authorizationLevel': 'OWN' - }] + }], + 'targets': { + 'enabledMetrics': [], + 'repeatsPerScenario': 1 + } }) mocker.patch.object(DB, 'update', return_value={}) - res = client.put('/api/v2/experiments/1', json={'experiment': { + res = client.put('/api/v2/portfolios/1', 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('/api/v2/experiments/1').status + assert '404' in client.delete('/api/v2/portfolios/1').status def test_delete_project_different_user(client, mocker): @@ -111,7 +127,7 @@ def test_delete_project_different_user(client, mocker): }] }) mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete('/api/v2/experiments/1').status + assert '403' in client.delete('/api/v2/portfolios/1').status def test_delete_project(client, mocker): @@ -121,7 +137,7 @@ def test_delete_project(client, mocker): '_id': '1', 'projectId': '1', 'googleId': 'test', - 'experimentIds': ['1'], + 'portfolioIds': ['1'], 'authorizations': [{ 'projectId': '1', 'authorizationLevel': 'OWN' @@ -129,5 +145,5 @@ def test_delete_project(client, mocker): }) mocker.patch.object(DB, 'delete_one', return_value={}) mocker.patch.object(DB, 'update', return_value=None) - res = client.delete('/api/v2/experiments/1') + res = client.delete('/api/v2/portfolios/1') assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/endpoint.py b/web-server/opendc/api/v2/projects/endpoint.py index 10954cdd..bf031382 100644 --- a/web-server/opendc/api/v2/projects/endpoint.py +++ b/web-server/opendc/api/v2/projects/endpoint.py @@ -19,7 +19,7 @@ def POST(request): project.set_property('datetimeCreated', Database.datetime_to_string(datetime.now())) project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) project.set_property('topologyIds', [topology.get_id()]) - project.set_property('experimentIds', []) + project.set_property('portfolioIds', []) project.insert() topology.set_property('projectId', project.get_id()) diff --git a/web-server/opendc/api/v2/projects/projectId/endpoint.py b/web-server/opendc/api/v2/projects/projectId/endpoint.py index 6f80f827..77b66d75 100644 --- a/web-server/opendc/api/v2/projects/projectId/endpoint.py +++ b/web-server/opendc/api/v2/projects/projectId/endpoint.py @@ -1,6 +1,6 @@ from datetime import datetime -from opendc.models.experiment import Experiment +from opendc.models.portfolio import Portfolio from opendc.models.project import Project from opendc.models.topology import Topology from opendc.models.user import User @@ -52,9 +52,9 @@ def DELETE(request): topology = Topology.from_id(topology_id) topology.delete() - for experiment_id in project.obj['experimentIds']: - experiment = Experiment.from_id(experiment_id) - experiment.delete() + for portfolio_id in project.obj['portfolioIds']: + portfolio = Portfolio.from_id(portfolio_id) + portfolio.delete() user = User.from_google_id(request.google_id) user.obj['authorizations'] = list( diff --git a/web-server/opendc/api/v2/projects/projectId/experiments/endpoint.py b/web-server/opendc/api/v2/projects/projectId/experiments/endpoint.py deleted file mode 100644 index 2e5b93df..00000000 --- a/web-server/opendc/api/v2/projects/projectId/experiments/endpoint.py +++ /dev/null @@ -1,35 +0,0 @@ -from opendc.models.experiment import Experiment -from opendc.models.project import Project -from opendc.util.rest import Response - - -def POST(request): - """Add a new Experiment for this Project.""" - - request.check_required_parameters(path={'projectId': 'string'}, - body={ - 'experiment': { - 'topologyId': 'string', - 'traceId': 'string', - 'schedulerName': 'string', - 'name': 'string', - } - }) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, True) - - experiment = Experiment(request.params_body['experiment']) - - experiment.set_property('projectId', request.params_path['projectId']) - experiment.set_property('state', 'QUEUED') - experiment.set_property('lastSimulatedTick', 0) - - experiment.insert() - - project.obj['experimentIds'].append(experiment.get_id()) - project.update() - - return Response(200, 'Successfully added Experiment.', experiment.obj) diff --git a/web-server/opendc/api/v2/projects/projectId/experiments/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/experiments/test_endpoint.py deleted file mode 100644 index 11b79154..00000000 --- a/web-server/opendc/api/v2/projects/projectId/experiments/test_endpoint.py +++ /dev/null @@ -1,78 +0,0 @@ -from opendc.util.database import DB - - -def test_add_experiment_missing_parameter(client): - assert '400' in client.post('/api/v2/projects/1/experiments').status - - -def test_add_experiment_non_existing_project(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.post('/api/v2/projects/1/experiments', - json={ - 'experiment': { - 'topologyId': '1', - 'traceId': '1', - 'schedulerName': 'default', - 'name': 'test', - } - }).status - - -def test_add_experiment_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'VIEW' - }] - }) - assert '403' in client.post('/api/v2/projects/1/experiments', - json={ - 'experiment': { - 'topologyId': '1', - 'traceId': '1', - 'schedulerName': 'default', - 'name': 'test', - } - }).status - - -def test_add_experiment(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': '1', - 'projectId': '1', - 'experimentIds': ['1'], - 'authorizations': [{ - 'projectId': '1', - 'authorizationLevel': 'EDIT' - }] - }) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': '1', - 'topologyId': '1', - 'traceId': '1', - 'schedulerName': 'default', - 'name': 'test', - 'state': 'QUEUED', - 'lastSimulatedTick': 0, - }) - mocker.patch.object(DB, 'update', return_value=None) - res = client.post( - '/api/v2/projects/1/experiments', - json={'experiment': { - 'topologyId': '1', - 'traceId': '1', - 'schedulerName': 'default', - 'name': 'test', - }}) - assert 'topologyId' in res.json['content'] - assert 'state' in res.json['content'] - assert 'lastSimulatedTick' in res.json['content'] - assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/__init__.py b/web-server/opendc/api/v2/projects/projectId/portfolios/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/web-server/opendc/api/v2/projects/projectId/portfolios/__init__.py diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py b/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py new file mode 100644 index 00000000..c51dee14 --- /dev/null +++ b/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py @@ -0,0 +1,36 @@ +from opendc.models.portfolio import Portfolio +from opendc.models.project import Project +from opendc.util.rest import Response + + +def POST(request): + """Add a new Portfolio for this Project.""" + + request.check_required_parameters(path={'projectId': 'string'}, + body={ + 'portfolio': { + 'name': 'string', + 'targets': { + 'enabledMetrics': 'list', + 'repeatsPerScenario': 'int', + }, + } + }) + + project = Project.from_id(request.params_path['projectId']) + + project.check_exists() + project.check_user_access(request.google_id, True) + + portfolio = Portfolio(request.params_body['portfolio']) + + portfolio.set_property('projectId', request.params_path['projectId']) + portfolio.set_property('scenarioIds', []) + portfolio.set_property('baseScenarioId', '-1') + + portfolio.insert() + + project.obj['portfolioIds'].append(portfolio.get_id()) + project.update() + + return Response(200, 'Successfully added Portfolio.', portfolio.obj) diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py new file mode 100644 index 00000000..5b4d9043 --- /dev/null +++ b/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py @@ -0,0 +1,85 @@ +from opendc.util.database import DB + + +def test_add_portfolio_missing_parameter(client): + assert '400' in client.post('/api/v2/projects/1/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('/api/v2/projects/1/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': '1', + 'projectId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + assert '403' in client.post('/api/v2/projects/1/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': '1', + 'projectId': '1', + 'portfolioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + mocker.patch.object(DB, + 'insert', + return_value={ + '_id': '1', + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + }, + 'projectId': '1', + 'scenarioIds': [], + 'baseScenarioId': '-1', + }) + mocker.patch.object(DB, 'update', return_value=None) + res = client.post( + '/api/v2/projects/1/portfolios', + json={ + 'portfolio': { + 'name': 'test', + 'targets': { + 'enabledMetrics': ['test'], + 'repeatsPerScenario': 2 + } + } + }) + assert 'projectId' in res.json['content'] + assert 'scenarioIds' in res.json['content'] + assert 'baseScenarioId' in res.json['content'] + assert '200' in res.status diff --git a/web-server/opendc/api/v2/projects/projectId/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/test_endpoint.py index c7450b5a..7a862e8d 100644 --- a/web-server/opendc/api/v2/projects/projectId/test_endpoint.py +++ b/web-server/opendc/api/v2/projects/projectId/test_endpoint.py @@ -111,7 +111,7 @@ def test_delete_project(client, mocker): 'authorizationLevel': 'OWN' }], 'topologyIds': [], - 'experimentIds': [], + 'portfolioIds': [], }) mocker.patch.object(DB, 'update', return_value=None) mocker.patch.object(DB, 'delete_one', return_value={'googleId': 'test'}) diff --git a/web-server/opendc/api/v2/scenarios/__init__.py b/web-server/opendc/api/v2/scenarios/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/web-server/opendc/api/v2/scenarios/__init__.py diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/__init__.py b/web-server/opendc/api/v2/scenarios/scenarioId/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/web-server/opendc/api/v2/scenarios/scenarioId/__init__.py diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py b/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py new file mode 100644 index 00000000..02d39063 --- /dev/null +++ b/web-server/opendc/api/v2/scenarios/scenarioId/endpoint.py @@ -0,0 +1,57 @@ +from opendc.models.scenario import Scenario +from opendc.models.portfolio import Portfolio +from opendc.util.rest import Response + + +def GET(request): + """Get this Scenario.""" + + request.check_required_parameters(path={'scenarioId': 'string'}) + + scenario = Scenario.from_id(request.params_path['scenarioId']) + + scenario.check_exists() + scenario.check_user_access(request.google_id, False) + + return Response(200, 'Successfully retrieved scenario.', scenario.obj) + + +def PUT(request): + """Update this Scenarios name.""" + + request.check_required_parameters(path={'scenarioId': 'string'}, body={'scenario': { + 'name': 'string', + }}) + + scenario = Scenario.from_id(request.params_path['scenarioId']) + + scenario.check_exists() + scenario.check_user_access(request.google_id, True) + + scenario.set_property('name', + request.params_body['scenario']['name']) + + scenario.update() + + return Response(200, 'Successfully updated scenario.', scenario.obj) + + +def DELETE(request): + """Delete this Scenario.""" + + request.check_required_parameters(path={'scenarioId': 'string'}) + + scenario = Scenario.from_id(request.params_path['scenarioId']) + + scenario.check_exists() + scenario.check_user_access(request.google_id, True) + + portfolio = Portfolio.from_id(scenario.obj['portfolioId']) + portfolio.check_exists() + if request.params_path['scenarioId'] in portfolio.obj['scenarioIds']: + portfolio.obj['scenarioIds'].remove(request.params_path['scenarioId']) + portfolio.update() + + old_object = scenario.delete() + + return Response(200, 'Successfully deleted scenario.', old_object) diff --git a/web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py b/web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py new file mode 100644 index 00000000..09b7d0c0 --- /dev/null +++ b/web-server/opendc/api/v2/scenarios/scenarioId/test_endpoint.py @@ -0,0 +1,140 @@ +from opendc.util.database import DB + + +def test_get_scenario_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.get('/api/v2/scenarios/1').status + + +def test_get_scenario_no_authorizations(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value={ + 'portfolioId': '1', + 'authorizations': [] + }) + res = client.get('/api/v2/scenarios/1') + assert '403' in res.status + + +def test_get_scenario_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'portfolioId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '2', + 'authorizationLevel': 'OWN' + }] + }) + res = client.get('/api/v2/scenarios/1') + assert '403' in res.status + + +def test_get_scenario(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + 'portfolioId': '1', + '_id': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'EDIT' + }] + }) + res = client.get('/api/v2/scenarios/1') + assert '200' in res.status + + +def test_update_scenario_missing_parameter(client): + assert '400' in client.put('/api/v2/scenarios/1').status + + +def test_update_scenario_non_existing(client, mocker): + mocker.patch.object(DB, 'fetch_one', return_value=None) + assert '404' in client.put('/api/v2/scenarios/1', json={ + 'scenario': { + 'name': 'test', + } + }).status + + +def test_update_scenario_not_authorized(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'update', return_value={}) + assert '403' in client.put('/api/v2/scenarios/1', json={ + 'scenario': { + 'name': 'test', + } + }).status + + +def test_update_scenario(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }], + 'targets': { + 'enabledMetrics': [], + 'repeatsPerScenario': 1 + } + }) + mocker.patch.object(DB, 'update', return_value={}) + + res = client.put('/api/v2/scenarios/1', 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('/api/v2/scenarios/1').status + + +def test_delete_project_different_user(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'googleId': 'other_test', + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'VIEW' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value=None) + assert '403' in client.delete('/api/v2/scenarios/1').status + + +def test_delete_project(client, mocker): + mocker.patch.object(DB, + 'fetch_one', + return_value={ + '_id': '1', + 'portfolioId': '1', + 'googleId': 'test', + 'scenarioIds': ['1'], + 'authorizations': [{ + 'projectId': '1', + 'authorizationLevel': 'OWN' + }] + }) + mocker.patch.object(DB, 'delete_one', return_value={}) + mocker.patch.object(DB, 'update', return_value=None) + res = client.delete('/api/v2/scenarios/1') + assert '200' in res.status diff --git a/web-server/opendc/models/model.py b/web-server/opendc/models/model.py index cab283c9..bcb833ae 100644 --- a/web-server/opendc/models/model.py +++ b/web-server/opendc/models/model.py @@ -33,8 +33,12 @@ class Model: raise ClientError(Response(404, 'Not found.')) def set_property(self, key, value): - """Sets the given property on the enclosed object.""" - self.obj[key] = value + """Sets the given property on the enclosed object, with support for simple nested access.""" + if '.' in key: + keys = key.split('.') + self.obj[keys[0]][keys[1]] = value + else: + self.obj[key] = value def insert(self): """Inserts the enclosed object and generates a UUID for it.""" diff --git a/web-server/opendc/models/experiment.py b/web-server/opendc/models/portfolio.py index 46373b99..32961b63 100644 --- a/web-server/opendc/models/experiment.py +++ b/web-server/opendc/models/portfolio.py @@ -4,10 +4,10 @@ from opendc.util.exceptions import ClientError from opendc.util.rest import Response -class Experiment(Model): - """Model representing a Experiment.""" +class Portfolio(Model): + """Model representing a Portfolio.""" - collection_name = 'experiments' + collection_name = 'portfolios' def check_user_access(self, google_id, edit_access): """Raises an error if the user with given [google_id] has insufficient access. @@ -21,4 +21,4 @@ class Experiment(Model): authorizations = list( filter(lambda x: str(x['projectId']) == str(self.obj['projectId']), user.obj['authorizations'])) if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, 'Forbidden from retrieving/editing experiment.')) + raise ClientError(Response(403, 'Forbidden from retrieving/editing portfolio.')) diff --git a/web-server/opendc/models/scenario.py b/web-server/opendc/models/scenario.py new file mode 100644 index 00000000..d7d959ca --- /dev/null +++ b/web-server/opendc/models/scenario.py @@ -0,0 +1,26 @@ +from opendc.models.model import Model +from opendc.models.portfolio import Portfolio +from opendc.models.user import User +from opendc.util.exceptions import ClientError +from opendc.util.rest import Response + + +class Scenario(Model): + """Model representing a Scenario.""" + + collection_name = 'scenarios' + + def check_user_access(self, google_id, edit_access): + """Raises an error if the user with given [google_id] has insufficient access. + + Checks access on the parent project. + + :param google_id: The Google ID of the user. + :param edit_access: True when edit access should be checked, otherwise view access. + """ + portfolio = Portfolio.from_id(self.obj['portfolioId']) + user = User.from_google_id(google_id) + authorizations = list( + filter(lambda x: str(x['projectId']) == str(portfolio.get_id()), user.obj['authorizations'])) + if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): + raise ClientError(Response(403, 'Forbidden from retrieving/editing scenario.')) diff --git a/web-server/opendc/util/parameter_checker.py b/web-server/opendc/util/parameter_checker.py index d37256e0..d1256009 100644 --- a/web-server/opendc/util/parameter_checker.py +++ b/web-server/opendc/util/parameter_checker.py @@ -46,14 +46,18 @@ def _incorrect_parameter(params_required, params_actual, parent=''): except: return '{}.{}'.format(parent, param_name) - if param_required == 'int' and not isinstance(param_actual, int): - return '{}.{}'.format(parent, param_name) - - if param_required == 'string' and not isinstance(param_actual, str) and not isinstance(param_actual, int): - return '{}.{}'.format(parent, param_name) - - if param_required.startswith('list') and not isinstance(param_actual, list): - return '{}.{}'.format(parent, param_name) + type_pairs = [ + ('int', (int,)), + ('float', (float,)), + ('bool', (bool,)), + ('string', (str, int)), + ('list', (list,)), + ] + + for str_type, actual_types in type_pairs: + if param_required == str_type and all(not isinstance(param_actual, t) + for t in actual_types): + return '{}.{}'.format(parent, param_name) return None |
