diff --git a/pyproject.toml b/pyproject.toml index 7338d0b3b..c47d602d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,3 +119,4 @@ unfixable = [ "__init__.py" = ["E402", "F401"] "pycroft/model/_all.py" = ["F403"] "helpers/interactive.py" = ["F403"] +"**/*.pyi" = ["F811"] diff --git a/tests/factories/property.py b/tests/factories/property.py index f4d606aab..8138d0f4e 100644 --- a/tests/factories/property.py +++ b/tests/factories/property.py @@ -59,12 +59,21 @@ def property_grants(self): class AdminPropertyGroupFactory(PropertyGroupFactory): name = factory.Sequence(partial(_maybe_append_seq, prefix="Admin-Gruppe")) - granted = frozenset(( - 'user_show', 'user_change', 'user_mac_change', - 'infrastructure_show', 'infrastructure_change', - 'facilities_show', 'facilities_change', - 'groups_show', 'groups_change_membership', 'groups_change', - )) + granted = frozenset( + ( + "user_show", + "user_change", + "user_mac_change", + "hosts_change", + "infrastructure_show", + "infrastructure_change", + "facilities_show", + "facilities_change", + "groups_show", + "groups_change_membership", + "groups_change", + ) + ) permission_level = 10 diff --git a/tests/frontend/test_host.py b/tests/frontend/test_host.py new file mode 100644 index 000000000..d28d45892 --- /dev/null +++ b/tests/frontend/test_host.py @@ -0,0 +1,168 @@ +# Copyright (c) 2015 The Pycroft Authors. See the AUTHORS file. +# This file is part of the Pycroft project and licensed under the terms of +# the Apache License, Version 2.0. See the LICENSE file for details. + +import pytest +from flask import url_for + +from pycroft.model.host import Host +from pycroft.model.user import User +from tests import factories as f + +from .assertions import TestClient + + +@pytest.fixture(scope="module") +def client(module_test_client: TestClient) -> TestClient: + return module_test_client + + +@pytest.fixture(scope="module") +def owner(module_session) -> User: + return f.UserFactory() + + +@pytest.fixture(scope="module") +def host(module_session, owner) -> Host: + return f.HostFactory(owner=owner, room__patched_with_subnet=True) + + +@pytest.mark.usefixtures("admin_logged_in") +class TestHostDelete: + def test_delete_nonexistent_host(self, client): + client.assert_url_response_code( + url_for("host.host_delete", host_id=999), code=404 + ) + + def test_host_delete_successful(self, session, client, host, owner): + with client.flashes_message("Host.*gelöscht", category="success"): + client.assert_url_redirects( + url_for("host.host_delete", host_id=host.id), + method="POST", + ) + session.refresh(owner) + assert owner.hosts == [] + + def test_host_get_returns_form(self, client, host): + with client.renders_template("generic_form.html"): + client.assert_url_ok(url_for("host.host_delete", host_id=host.id)) + + +@pytest.mark.usefixtures("admin_logged_in") +class TestHostEdit: + def test_edit_nonexistent_host(self, client): + client.assert_url_response_code( + url_for("host.host_edit", host_id=999), code=404 + ) + + def test_edit_host_get(self, client, host): + with client.renders_template("generic_form.html"): + client.assert_url_ok(url_for("host.host_edit", host_id=host.id)) + + def test_post_without_data(self, client, host): + """works because the room data is automatically derived from the host""" + # HTTP 200 OK although form invalid + client.assert_url_ok( + url_for("host.host_edit", host_id=host.id), + method="POST", + ) + + def test_post_with_data(self, client, host): + with client.flashes_message("Host.*bearbeitet", category="success"): + client.assert_url_redirects( + url_for("host.host_edit", host_id=host.id), + method="POST", + data={ + "owner": host.owner.id, + "name": f"new-{host.name}", + "building": host.room.building.id, + "level": host.room.level, + "room_number": host.room.number, + }, + ) + + def test_post_with_invalid_data(self, client, host): + client.assert_url_ok( + url_for("host.host_edit", host_id=host.id), + method="POST", + data={ + "owner": host.owner.id, + "name": f"new-{host.name}", + "building": host.room.building.id, + "level": host.room.level, + "room_number": 999, + }, + ) + + +@pytest.mark.usefixtures("admin_logged_in") +class TestHostCreate: + def test_create_host_nonexistent_owner(self, client): + client.assert_url_response_code( + url_for("host.host_create", user_id=999), code=404 + ) + + def test_create_host_get(self, client, owner): + with client.renders_template("generic_form.html"): + client.assert_url_ok(url_for("host.host_create", user_id=owner.id)) + + def test_create_host_post(self, session, client, owner, host): + with client.flashes_message("Host.*erstellt", category="success"): + client.assert_url_redirects( + url_for("host.host_create", user_id=owner.id), + method="POST", + data={ + "name": "test-host", + "building": owner.room.building.id, + "level": owner.room.level, + "room_number": owner.room.number, + }, + ) + session.refresh(owner) + # assert len(owner.hosts) == 1 + # assert owner.hosts[0].name == "test-host" + new_hosts = set(owner.hosts) - {host} + assert len(new_hosts) == 1 + assert list(new_hosts)[0].name == "test-host" + + def test_create_host_post_invalid_data(self, session, client, owner): + client.assert_url_ok( + url_for("host.host_create", user_id=owner.id), + method="POST", + data={ + "name": "test-host", + "building": owner.room.building.id, + "level": owner.room.level, + "room_number": 999, + }, + ) + session.refresh(owner) + assert len(owner.hosts) == 1 + + +def test_user_hosts(client, host): + resp = client.assert_url_ok(url_for("host.user_hosts_json", user_id=host.owner.id)) + + assert "items" in resp.json + items = resp.json["items"] + assert len(items) == 1 + [item] = items + assert item["switch"] + assert item["port"] + assert item["id"] == host.id + assert len(item["actions"]) == 2 + + +@pytest.fixture() +def host_without_room(session): + return f.HostFactory(room=None) + + +def test_user_host_without_room(client, host_without_room): + resp = client.assert_url_ok( + url_for("host.user_hosts_json", user_id=host_without_room.owner.id) + ) + assert len(resp.json["items"]) == 1 + [it] = resp.json["items"] + assert it["switch"] is None + assert it["port"] is None diff --git a/tests/frontend/test_interface.py b/tests/frontend/test_interface.py new file mode 100644 index 000000000..7459bd30d --- /dev/null +++ b/tests/frontend/test_interface.py @@ -0,0 +1,154 @@ +# Copyright (c) 2015 The Pycroft Authors. See the AUTHORS file. +# This file is part of the Pycroft project and licensed under the terms of +# the Apache License, Version 2.0. See the LICENSE file for details. + +import pytest +from flask import url_for + +from pycroft.model.host import Host, Interface +from tests import factories as f + +from .assertions import TestClient + + +@pytest.fixture(scope="module") +def client(module_test_client: TestClient) -> TestClient: + return module_test_client + + +@pytest.fixture(scope="module", autouse=True) +def host(module_session) -> Host: + return f.HostFactory(interface=None, room__patched_with_subnet=True) + + +@pytest.fixture(scope="module", autouse=True) +def interface(module_session, host) -> Interface: + return f.InterfaceFactory(host=host) + + +pytestmark = pytest.mark.usefixtures("admin_logged_in") + + +class TestInterfacesJson: + def test_interfaces_nonexistent_host(self, client): + client.assert_url_response_code( + url_for("host.host_interfaces_json", host_id=999), code=404 + ) + + def test_interfaces_json(self, client, interface): + resp = client.assert_url_ok( + url_for("host.host_interfaces_json", host_id=interface.host.id) + ) + + assert "items" in resp.json + items = resp.json["items"] + assert len(items) == 1 + [item] = items + assert item["id"] == interface.id + assert len(item["actions"]) == 2 + assert item["ips"] != "" + + +def test_interface_table(client, interface): + with client.renders_template("host/interface_table.html"): + client.assert_url_ok(url_for("host.interface_table", host_id=interface.host_id)) + + +@pytest.mark.usefixtures("session") +class TestInterfaceDelete: + def test_delete_nonexistent_interface(self, client): + client.assert_url_response_code( + url_for("host.interface_delete", interface_id=999), code=404 + ) + + def test_delete_interface_get(self, client, interface): + with client.renders_template("generic_form.html"): + client.assert_url_ok( + url_for("host.interface_delete", interface_id=interface.id) + ) + + def test_delete_interface_post(self, session, client, interface, host): + with client.flashes_message("Interface.*gelöscht", category="success"): + client.assert_url_redirects( + url_for("host.interface_delete", interface_id=interface.id), + method="POST", + ) + session.refresh(host) + assert interface not in host.interfaces + + +@pytest.mark.usefixtures("session") +class TestInterfaceEdit: + def test_edit_nonexistent_interface(self, client): + client.assert_url_response_code( + url_for("host.interface_edit", interface_id=999), code=404 + ) + + def test_edit_interface_get(self, client, interface): + with client.renders_template("generic_form.html"): + client.assert_url_ok( + url_for("host.interface_edit", interface_id=interface.id) + ) + + def test_edit_interface_post_invalid_data(self, client, interface): + with client.renders_template("generic_form.html"): + client.assert_url_ok( + url_for("host.interface_edit", interface_id=interface.id), + data={"mac": "invalid"}, + method="POST", + ) + + def test_edit_interface_success(self, session, client, interface): + with client.flashes_message("Interface.*bearbeitet", category="success"): + client.assert_url_redirects( + url_for("host.interface_edit", interface_id=interface.id), + method="POST", + data={"mac": "00:11:22:33:44:55", "name": "new name"}, + ) + session.refresh(interface) + assert interface.mac == "00:11:22:33:44:55" + assert interface.name == "new name" + + +@pytest.mark.usefixtures("session") +class TestInterfaceCreate: + def test_create_interface_nonexistent_host(self, client, host): + client.assert_url_response_code( + url_for("host.interface_create", host_id=999), code=404 + ) + + def test_create_interface_get(self, client, host): + with client.renders_template("generic_form.html"): + client.assert_url_ok(url_for("host.interface_create", host_id=host.id)) + + def test_create_interface_post_invalid_data(self, client, host): + with client.renders_template("generic_form.html"): + client.assert_url_ok( + url_for("host.interface_create", host_id=host.id), + method="POST", + data={"mac": "invalid"}, + ) + + def test_create_interface_success(self, client, host): + with client.flashes_message("Interface.*erstellt", category="success"): + client.assert_url_redirects( + url_for("host.interface_create", host_id=host.id), + method="POST", + data={"mac": "00:11:22:33:44:55", "name": "new name"}, + ) + + +class TestManufacturer: + def test_manufacturer_invalid_mac(self, client): + client.assert_url_response_code( + url_for("host.interface_manufacturer_json", mac="invalid"), code=400 + ) + + def test_manufacturer_valid_mac(self, client, monkeypatch): + monkeypatch.setattr( + "web.blueprints.host.get_interface_manufacturer", lambda mac: "AG DSN" + ) + resp = client.assert_url_ok( + url_for("host.interface_manufacturer_json", mac="00:11:22:33:44:55") + ) + assert resp.json["manufacturer"] == "AG DSN" diff --git a/web/blueprints/host/__init__.py b/web/blueprints/host/__init__.py index fcf48e0b6..404bb7bb5 100644 --- a/web/blueprints/host/__init__.py +++ b/web/blueprints/host/__init__.py @@ -55,7 +55,7 @@ def default_response(): with handle_errors(session.session): lib_host.host_delete(host, current_user) session.session.commit() - except PycroftException: + except PycroftException: # pragma: no cover return default_response() flash("Host erfolgreich gelöscht.", 'success') @@ -105,7 +105,7 @@ def default_response(): processor=current_user ) session.session.commit() - except PycroftException: + except PycroftException: # pragma: no cover return default_response() flash("Host erfolgreich bearbeitet.", 'success') @@ -157,13 +157,15 @@ def default_response(): owner, room, form.name.data, processor=current_user ) session.session.commit() - except PycroftException: + except PycroftException: # pragma: no cover return default_response() - return redirect(url_for( - '.interface_create', - user_id=host.owner_id, host_id=host.id, _anchor='hosts' - )) + flash("Host erfolgreich erstellt.", "success") + return redirect( + url_for( + ".interface_create", user_id=host.owner_id, host_id=host.id, _anchor="hosts" + ) + ) @bp.route("//interfaces") @@ -240,7 +242,7 @@ def default_response(): with handle_errors(session.session): lib_host.interface_delete(interface, current_user) session.session.commit() - except PycroftException: + except PycroftException: # pragma: no cover return default_response() flash("Interface erfolgreich gelöscht.", 'success') @@ -278,7 +280,7 @@ def default_response(): if not form.is_submitted(): form.ips.process_data(ip for ip in current_ips) return default_response() - if not form.validate: + if not form.validate(): return default_response() ips = {IPv4Address(ip) for ip in form.ips.data} @@ -290,7 +292,7 @@ def default_response(): processor=current_user ) session.session.commit() - except PycroftException: + except PycroftException: # pragma: no cover return default_response() flash("Interface erfolgreich bearbeitet.", 'success') @@ -336,7 +338,7 @@ def default_response(): current_user ) session.session.commit() - except PycroftException: + except PycroftException: # pragma: no cover return default_response() flash("Interface erfolgreich erstellt.", 'success') @@ -356,7 +358,9 @@ def user_hosts_json(user_id): if host.room: patch_ports = host.room.connected_patch_ports switches = ', '.join( - p.switch_port.switch.host.name for p in patch_ports) + p.switch_port.switch.host.name or "" + for p in patch_ports + ) ports = ', '.join(p.switch_port.name for p in patch_ports) else: switches = None