From 3f2391c79e86cbfed18ad446a11d355c2a40bea1 Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand <benjamin.bertrand@ess.eu> Date: Sat, 7 Mar 2020 21:41:28 +0100 Subject: [PATCH] Add DELETE method on all API endpoints This is for admin only. Note that deleting a network with existing hosts is not allowed. Same for domain, network scope... Will make testing csentry-api easier by allowing to reset the db during tests. JIRA INFRA-1853 #action In Progress --- app/api/inventory.py | 74 +++++++++++++++++- app/api/network.py | 60 +++++++++++++++ app/api/utils.py | 2 +- tests/functional/factories.py | 1 + tests/functional/test_api.py | 138 +++++++++++++++++++++++++++++++--- tests/functional/test_web.py | 2 +- 6 files changed, 263 insertions(+), 14 deletions(-) diff --git a/app/api/inventory.py b/app/api/inventory.py index 914fcb5..aef68cc 100644 --- a/app/api/inventory.py +++ b/app/api/inventory.py @@ -13,7 +13,7 @@ from flask import Blueprint, jsonify, request, current_app from flask_login import login_required from .. import utils, models from ..decorators import login_groups_accepted -from .utils import commit, create_generic_model, get_generic_model +from .utils import commit, create_generic_model, get_generic_model, delete_generic_model bp = Blueprint("inventory_api", __name__) @@ -140,6 +140,18 @@ def patch_item(id_): return jsonify(item.to_dict()) +@bp.route("/items/<int:item_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_item(item_id): + """Delete an item + + .. :quickref: Inventory; Delete an item + + :param item_id: item primary key + """ + return delete_generic_model(models.Item, item_id) + + @bp.route("/items/<id_>/comments") @login_required def get_item_comments(id_): @@ -169,6 +181,18 @@ def create_item_comment(id_): ) +@bp.route("/items/comments/<int:comment_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_comment(comment_id): + """Delete an item comment + + .. :quickref: Inventory; Delete an item comment + + :param comment_id: comment primary key + """ + return delete_generic_model(models.ItemComment, comment_id) + + @bp.route("/actions") @login_required def get_actions(): @@ -202,6 +226,18 @@ def create_manufacturer(): return create_generic_model(models.Manufacturer) +@bp.route("/manufacturers/<int:manufacturer_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_manufacturer(manufacturer_id): + """Delete a manufacturer + + .. :quickref: Inventory; Delete a manufacturer + + :param manufacturer_id: manufacturer primary key + """ + return delete_generic_model(models.Manufacturer, manufacturer_id) + + @bp.route("/models") @login_required def get_models(): @@ -225,6 +261,18 @@ def create_model(): return create_generic_model(models.Model) +@bp.route("/models/<int:model_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_model(model_id): + """Delete a model + + .. :quickref: Inventory; Delete a model + + :param model_id: model primary key + """ + return delete_generic_model(models.Model, model_id) + + @bp.route("/locations") @login_required def get_locations(): @@ -248,6 +296,18 @@ def create_locations(): return create_generic_model(models.Location) +@bp.route("/locations/<int:location_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_location(location_id): + """Delete a location + + .. :quickref: Inventory; Delete a location + + :param location_id: location primary key + """ + return delete_generic_model(models.Location, location_id) + + @bp.route("/statuses") @login_required def get_status(): @@ -271,6 +331,18 @@ def create_status(): return create_generic_model(models.Status) +@bp.route("/statuses/<int:status_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_status(status_id): + """Delete a status + + .. :quickref: Inventory; Delete a status + + :param status_id: status primary key + """ + return delete_generic_model(models.Status, status_id) + + @bp.route("/macs") @login_required def get_macs(): diff --git a/app/api/network.py b/app/api/network.py index a293254..be24d54 100644 --- a/app/api/network.py +++ b/app/api/network.py @@ -52,6 +52,18 @@ def create_scope(): ) +@bp.route("/scopes/<int:scope_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_scope(scope_id): + """Delete a network scope + + .. :quickref: Network; Delete a network scope + + :param scope_id: network scope primary key + """ + return utils.delete_generic_model(models.NetworkScope, scope_id) + + @bp.route("/networks") @login_groups_accepted("admin") def get_networks(): @@ -133,6 +145,18 @@ def patch_network(network_id): return utils.update_generic_model(models.Network, network_id, allowed_fields) +@bp.route("/networks/<int:network_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_network(network_id): + """Delete a network + + .. :quickref: Network; Delete a network + + :param network_id: network primary key + """ + return utils.delete_generic_model(models.Network, network_id) + + @bp.route("/interfaces") @login_required def get_interfaces(): @@ -274,6 +298,18 @@ def create_ansible_groups(): return utils.create_generic_model(models.AnsibleGroup, mandatory_fields=("name",)) +@bp.route("/groups/<int:group_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_ansible_group(group_id): + """Delete an Ansible group + + .. :quickref: Network; Delete an Ansible group + + :param group_id: Ansible group primary key + """ + return utils.delete_generic_model(models.AnsibleGroup, group_id) + + @bp.route("/hosts") @login_required def get_hosts(): @@ -393,6 +429,18 @@ def create_domain(): return utils.create_generic_model(models.Domain, mandatory_fields=("name",)) +@bp.route("/domains/<int:domain_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_domain(domain_id): + """Delete a domain + + .. :quickref: Network; Delete a domain + + :param domain_id: domain primary key + """ + return utils.delete_generic_model(models.Domain, domain_id) + + @bp.route("/cnames") @login_required def get_cnames(): @@ -427,3 +475,15 @@ def create_cname(): return utils.create_generic_model( models.Cname, mandatory_fields=("name", "interface_id") ) + + +@bp.route("/cnames/<int:cname_id>", methods=["DELETE"]) +@login_groups_accepted("admin") +def delete_cname(cname_id): + """Delete a cname + + .. :quickref: Network; Delete a cname + + :param cname_id: cname primary key + """ + return utils.delete_generic_model(models.Cname, cname_id) diff --git a/app/api/utils.py b/app/api/utils.py index 8468eb8..8091518 100644 --- a/app/api/utils.py +++ b/app/api/utils.py @@ -165,7 +165,7 @@ def delete_generic_model(model, primary_key): """ instance = model.query.get_or_404(primary_key) db.session.delete(instance) - db.session.commit() + commit() return jsonify(), 204 diff --git a/tests/functional/factories.py b/tests/functional/factories.py index 60bbb8c..33722af 100644 --- a/tests/functional/factories.py +++ b/tests/functional/factories.py @@ -93,6 +93,7 @@ class ItemCommentFactory(factory.alchemy.SQLAlchemyModelFactory): body = factory.Sequence(lambda n: f"comment{n}") user = factory.SubFactory(UserFactory) + item = factory.SubFactory(ItemFactory) class DomainFactory(factory.alchemy.SQLAlchemyModelFactory): diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py index 57b6d07..719e33e 100644 --- a/tests/functional/test_api.py +++ b/tests/functional/test_api.py @@ -222,6 +222,12 @@ def check_input_is_subset_of_response(response, inputs): assert d2[key] == value +def check_delete_success(clt, token, instance, endpoint, model): + response = delete(clt, f"{API_URL}/{endpoint}/{instance.id}", token=token) + assert response.status_code == 204 + assert len(model.query.all()) == 0 + + def test_login(client): response = client.post(f"{API_URL}/user/login") check_response_message(response, "Body should be a JSON object") @@ -1355,13 +1361,10 @@ def test_delete_interface_invalid_credentials(client, interface_factory, user_to assert len(models.Interface.query.all()) == 1 -def test_delete_interface_success(client, interface_factory, admin_token): - interface1 = interface_factory() - response = delete( - client, f"{API_URL}/network/interfaces/{interface1.id}", token=admin_token +def test_delete_interface_success(client, admin_token, interface): + check_delete_success( + client, admin_token, interface, "network/interfaces", models.Interface, ) - assert response.status_code == 204 - assert len(models.Interface.query.all()) == 0 def test_delete_interface_invalid_id(client, interface_factory, admin_token): @@ -1745,11 +1748,10 @@ def test_delete_host_invalid_credentials(client, host_factory, user_token): assert len(models.Host.query.all()) == 1 -def test_delete_host_success(client, host_factory, admin_token): - host1 = host_factory() - response = delete(client, f"{API_URL}/network/hosts/{host1.id}", token=admin_token) - assert response.status_code == 204 - assert len(models.Host.query.all()) == 0 +def test_delete_host_success(client, admin_token, host): + check_delete_success( + client, admin_token, host, "network/hosts", models.Host, + ) def test_delete_host_invalid_id(client, host_factory, admin_token): @@ -2553,3 +2555,117 @@ def test_patch_network_invalid_domain(client, network_factory, admin_token): client, f"{API_URL}/network/networks/{network.id}", data=data, token=admin_token ) check_response_message(response, f"foo is not a valid domain", 400) + + +def test_delete_item_success(client, admin_token, item): + check_delete_success( + client, admin_token, item, "inventory/items", models.Item, + ) + + +def test_delete_item_comment_success(client, admin_token, item_comment): + check_delete_success( + client, + admin_token, + item_comment, + "inventory/items/comments", + models.ItemComment, + ) + + +def test_delete_manufacturer_success(client, admin_token, manufacturer): + check_delete_success( + client, + admin_token, + manufacturer, + "inventory/manufacturers", + models.Manufacturer, + ) + + +def test_delete_model_success(client, admin_token, model): + check_delete_success( + client, admin_token, model, "inventory/models", models.Model, + ) + + +def test_delete_location_success(client, admin_token, location): + check_delete_success( + client, admin_token, location, "inventory/locations", models.Location, + ) + + +def test_delete_status_success(client, admin_token, status): + check_delete_success( + client, admin_token, status, "inventory/statuses", models.Status, + ) + + +def test_delete_network_scope_success(client, admin_token, network_scope): + check_delete_success( + client, admin_token, network_scope, "network/scopes", models.NetworkScope, + ) + + +def test_delete_network_scope_with_network_fail( + client, network_scope, network_factory, admin_token +): + network_factory(scope=network_scope) + response = delete( + client, f"{API_URL}/network/scopes/{network_scope.id}", token=admin_token + ) + check_response_message( + response, + '(psycopg2.IntegrityError) null value in column "scope_id" violates not-null constraint', + 422, + ) + + +def test_delete_network_success(client, admin_token, network): + check_delete_success( + client, admin_token, network, "network/networks", models.Network, + ) + + +def test_delete_network_with_host_fail( + client, network_192_168_1, interface_factory, admin_token +): + interface_factory(network=network_192_168_1) + response = delete( + client, f"{API_URL}/network/networks/{network_192_168_1.id}", token=admin_token + ) + check_response_message( + response, + '(psycopg2.IntegrityError) null value in column "network_id" violates not-null constraint', + 422, + ) + + +def test_delete_ansible_group_success(client, admin_token, ansible_group): + check_delete_success( + client, admin_token, ansible_group, "network/groups", models.AnsibleGroup, + ) + + +def test_delete_domain_success(client, admin_token, domain): + check_delete_success( + client, admin_token, domain, "network/domains", models.Domain, + ) + + +def test_delete_domain_with_network_fail(client, domain, network_factory, admin_token): + network_factory(domain=domain) + response = delete( + client, f"{API_URL}/network/domains/{domain.id}", token=admin_token + ) + check_response_message( + response, + '(psycopg2.IntegrityError) null value in column "domain_id" violates not-null constraint', + 422, + ) + + +def test_delete_cname_success(client, admin_token, cname): + check_delete_success( + client, admin_token, cname, "network/cnames", models.Cname, + ) diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py index 9fa25e8..eed25c3 100644 --- a/tests/functional/test_web.py +++ b/tests/functional/test_web.py @@ -336,7 +336,7 @@ def test_edit_item_comment_in_index( logged_rw_client, item_factory, item_comment_factory ): item1 = item_factory(ics_id="AAA001") - comment = item_comment_factory(body="Hello", item_id=item1.id) + comment = item_comment_factory(body="Hello", item=item1) assert item1.comments == [comment] # Edit the comment body = "Hello world!" -- GitLab