diff --git a/hcloud/zones/client.py b/hcloud/zones/client.py index 09fbd277..baa841be 100644 --- a/hcloud/zones/client.py +++ b/hcloud/zones/client.py @@ -352,7 +352,7 @@ def update_rrset( DNS API is in beta, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#2025-10-07-dns-beta for more details. """ - return self._client.update_rrset(rrset, labels=labels) + return self._client.update_rrset(rrset=rrset, labels=labels) def delete_rrset( self, @@ -369,7 +369,7 @@ def delete_rrset( DNS API is in beta, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#2025-10-07-dns-beta for more details. """ - return self._client.delete_rrset(rrset) + return self._client.delete_rrset(rrset=rrset) def change_rrset_protection( self, @@ -389,7 +389,7 @@ def change_rrset_protection( DNS API is in beta, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#2025-10-07-dns-beta for more details. """ - return self._client.change_rrset_protection(rrset, change=change) + return self._client.change_rrset_protection(rrset=rrset, change=change) def change_rrset_ttl( self, @@ -408,7 +408,7 @@ def change_rrset_ttl( DNS API is in beta, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#2025-10-07-dns-beta for more details. """ - return self._client.change_rrset_ttl(rrset, ttl=ttl) + return self._client.change_rrset_ttl(rrset=rrset, ttl=ttl) def add_rrset_records( self, @@ -429,7 +429,7 @@ def add_rrset_records( DNS API is in beta, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#2025-10-07-dns-beta for more details. """ - return self._client.add_rrset_records(rrset, records=records, ttl=ttl) + return self._client.add_rrset_records(rrset=rrset, records=records, ttl=ttl) def remove_rrset_records( self, @@ -448,7 +448,7 @@ def remove_rrset_records( DNS API is in beta, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#2025-10-07-dns-beta for more details. """ - return self._client.remove_rrset_records(rrset, records=records) + return self._client.remove_rrset_records(rrset=rrset, records=records) def set_rrset_records( self, @@ -467,7 +467,7 @@ def set_rrset_records( DNS API is in beta, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#2025-10-07-dns-beta for more details. """ - return self._client.set_rrset_records(rrset, records=records) + return self._client.set_rrset_records(rrset=rrset, records=records) class BoundZoneRRSet(BoundModelBase, ZoneRRSet): diff --git a/tests/unit/certificates/test_client.py b/tests/unit/certificates/test_client.py index cd1c8a60..8182bb48 100644 --- a/tests/unit/certificates/test_client.py +++ b/tests/unit/certificates/test_client.py @@ -12,88 +12,42 @@ ManagedCertificateStatus, ) +from ..conftest import BoundModelTestCase -class TestBoundCertificate: - @pytest.fixture() - def bound_certificate(self, client: Client): - return BoundCertificate(client.certificates, data=dict(id=14)) - - def test_bound_certificate_init(self, certificate_response): - bound_certificate = BoundCertificate( - client=mock.MagicMock(), data=certificate_response["certificate"] - ) - - assert bound_certificate.id == 2323 - assert bound_certificate.name == "My Certificate" - assert bound_certificate.type == "managed" - assert ( - bound_certificate.fingerprint - == "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f" - ) - assert bound_certificate.certificate == "-----BEGIN CERTIFICATE-----\n..." - assert len(bound_certificate.domain_names) == 3 - assert bound_certificate.domain_names[0] == "example.com" - assert bound_certificate.domain_names[1] == "webmail.example.com" - assert bound_certificate.domain_names[2] == "www.example.com" - assert isinstance(bound_certificate.status, ManagedCertificateStatus) - assert bound_certificate.status.issuance == "failed" - assert bound_certificate.status.renewal == "scheduled" - assert bound_certificate.status.error.code == "error_code" - assert bound_certificate.status.error.message == "error message" - - def test_update( - self, - request_mock: mock.MagicMock, - bound_certificate, - response_update_certificate, - ): - request_mock.return_value = response_update_certificate - certificate = bound_certificate.update(name="New name") +class TestBoundCertificate(BoundModelTestCase): + methods = [ + BoundCertificate.update, + BoundCertificate.delete, + BoundCertificate.retry_issuance, + ] - request_mock.assert_called_with( - method="PUT", - url="/certificates/14", - json={"name": "New name"}, - ) - - assert certificate.id == 2323 - assert certificate.name == "New name" - - def test_delete( - self, - request_mock: mock.MagicMock, - bound_certificate, - action_response, - ): - request_mock.return_value = action_response - - delete_success = bound_certificate.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/certificates/14", - ) - - assert delete_success is True - - def test_retry_issuance( - self, - request_mock: mock.MagicMock, - bound_certificate, - response_retry_issuance_action, - ): - request_mock.return_value = response_retry_issuance_action - - action = bound_certificate.retry_issuance() + @pytest.fixture() + def resource_client(self, client: Client): + return client.certificates - request_mock.assert_called_with( - method="POST", - url="/certificates/14/actions/retry", + @pytest.fixture() + def bound_model(self, resource_client, certificate_response): + return BoundCertificate( + resource_client, data=certificate_response["certificate"] ) - assert action.id == 14 - assert action.command == "issue_certificate" + def test_init(self, bound_model: BoundCertificate): + o = bound_model + assert o.id == 2323 + assert o.name == "My Certificate" + assert o.type == "managed" + assert o.fingerprint == "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f" + assert o.certificate == "-----BEGIN CERTIFICATE-----\n..." + assert len(o.domain_names) == 3 + assert o.domain_names[0] == "example.com" + assert o.domain_names[1] == "webmail.example.com" + assert o.domain_names[2] == "www.example.com" + assert isinstance(o.status, ManagedCertificateStatus) + assert o.status.issuance == "failed" + assert o.status.renewal == "scheduled" + assert o.status.error.code == "error_code" + assert o.status.error.message == "error message" class TestCertificatesClient: diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index a340a6e3..4b83b01a 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -2,9 +2,9 @@ from __future__ import annotations -from collections.abc import Generator +import inspect +from typing import Callable, ClassVar, TypedDict from unittest import mock -from warnings import warn import pytest @@ -134,11 +134,100 @@ def action_list_response(action1_running, action2_running): } -@pytest.fixture() -def hetzner_client() -> Generator[Client]: - warn("DEPRECATED") - client = Client(token="token") - patcher = mock.patch.object(client, "request") - patcher.start() - yield client - patcher.stop() +def build_kwargs_mock(func: Callable) -> dict[str, mock.Mock]: + """ + Generate a kwargs dict that may be passed to the provided function for testing purposes. + """ + s = inspect.signature(func) + + kwargs = {} + for name, param in s.parameters.items(): + if name in ("self",): + continue + + if param.kind in (param.POSITIONAL_OR_KEYWORD, param.KEYWORD_ONLY): + kwargs[name] = mock.Mock() + continue + + # Ignore **kwargs + if param.kind in (param.VAR_KEYWORD,): + continue + + raise NotImplementedError(f"unsupported parameter kind: {param.kind}") + + return kwargs + + +def pytest_generate_tests(metafunc: pytest.Metafunc): + """ + Magic function to generate a test for each bound model method. + """ + if "bound_model_method" in metafunc.fixturenames: + metafunc.parametrize("bound_model_method", metafunc.cls.methods) + + +class BoundModelTestOptions(TypedDict): + sub_resource: bool + + +class BoundModelTestCase: + methods: ClassVar[list[Callable | tuple[Callable, BoundModelTestOptions]]] + + def test_method_list(self, bound_model): + """ + Ensure the list of bound model methods is up to date. + """ + # Unpack methods + methods = [m[0] if isinstance(m, tuple) else m for m in self.__class__.methods] + + members_count = 0 + members_missing = [] + for name, member in inspect.getmembers( + bound_model, + lambda m: inspect.ismethod(m) + and m.__func__ in bound_model.__class__.__dict__.values(), + ): + # Actions methods are already tested in TestBoundModelActions. + if name in ("__init__", "get_actions", "get_actions_list"): + continue + + if member.__func__ in methods: + members_count += 1 + else: + members_missing.append(member.__func__.__qualname__) + + assert not members_missing, "untested methods:\n" + ",\n".join(members_missing) + assert members_count == len(self.__class__.methods) + + def test_method( + self, + resource_client, + bound_model, + bound_model_method: Callable | tuple[Callable, BoundModelTestOptions], + ): + options = BoundModelTestOptions() + if isinstance(bound_model_method, tuple): + bound_model_method, options = bound_model_method + + # Check if the resource client has a method named after the bound model method. + assert hasattr(resource_client, bound_model_method.__name__) + + # Mock the resource client method. + resource_client_method_mock = mock.MagicMock() + setattr( + resource_client, + bound_model_method.__name__, + resource_client_method_mock, + ) + + kwargs = build_kwargs_mock(bound_model_method) + + # Call the bound model method + result = getattr(bound_model, bound_model_method.__name__)(**kwargs) + + if options.get("sub_resource"): + resource_client_method_mock.assert_called_with(**kwargs) + else: + resource_client_method_mock.assert_called_with(bound_model, **kwargs) + + assert result is resource_client_method_mock.return_value diff --git a/tests/unit/firewalls/test_client.py b/tests/unit/firewalls/test_client.py index 78b66409..79313c15 100644 --- a/tests/unit/firewalls/test_client.py +++ b/tests/unit/firewalls/test_client.py @@ -15,31 +15,42 @@ ) from hcloud.servers import Server +from ..conftest import BoundModelTestCase -class TestBoundFirewall: - @pytest.fixture() - def bound_firewall(self, client: Client): - return BoundFirewall(client.firewalls, data=dict(id=1)) - - def test_bound_firewall_init(self, firewall_response): - bound_firewall = BoundFirewall( - client=mock.MagicMock(), data=firewall_response["firewall"] - ) - assert bound_firewall.id == 38 - assert bound_firewall.name == "Corporate Intranet Protection" - assert bound_firewall.labels == {} - assert isinstance(bound_firewall.rules, list) - assert len(bound_firewall.rules) == 2 +class TestBoundFirewall(BoundModelTestCase): + methods = [ + BoundFirewall.update, + BoundFirewall.delete, + BoundFirewall.apply_to_resources, + BoundFirewall.remove_from_resources, + BoundFirewall.set_rules, + ] - assert isinstance(bound_firewall.applied_to, list) - assert len(bound_firewall.applied_to) == 2 - assert bound_firewall.applied_to[0].server.id == 42 - assert bound_firewall.applied_to[0].type == "server" - assert bound_firewall.applied_to[1].label_selector.selector == "key==value" - assert bound_firewall.applied_to[1].type == "label_selector" + @pytest.fixture() + def resource_client(self, client: Client): + return client.firewalls - firewall_in_rule = bound_firewall.rules[0] + @pytest.fixture() + def bound_model(self, resource_client, firewall_response): + return BoundFirewall(resource_client, data=firewall_response["firewall"]) + + def test_init(self, bound_model: BoundFirewall): + o = bound_model + assert o.id == 38 + assert o.name == "Corporate Intranet Protection" + assert o.labels == {} + assert isinstance(o.rules, list) + assert len(o.rules) == 2 + + assert isinstance(o.applied_to, list) + assert len(o.applied_to) == 2 + assert o.applied_to[0].server.id == 42 + assert o.applied_to[0].type == "server" + assert o.applied_to[1].label_selector.selector == "key==value" + assert o.applied_to[1].type == "label_selector" + + firewall_in_rule = o.rules[0] assert isinstance(firewall_in_rule, FirewallRule) assert firewall_in_rule.direction == FirewallRule.DIRECTION_IN assert firewall_in_rule.protocol == FirewallRule.PROTOCOL_TCP @@ -55,7 +66,7 @@ def test_bound_firewall_init(self, firewall_response): assert len(firewall_in_rule.destination_ips) == 0 assert firewall_in_rule.description == "allow http in" - firewall_out_rule = bound_firewall.rules[1] + firewall_out_rule = o.rules[1] assert isinstance(firewall_out_rule, FirewallRule) assert firewall_out_rule.direction == FirewallRule.DIRECTION_OUT assert firewall_out_rule.protocol == FirewallRule.PROTOCOL_TCP @@ -71,120 +82,6 @@ def test_bound_firewall_init(self, firewall_response): ] assert firewall_out_rule.description == "allow http out" - def test_update( - self, - request_mock: mock.MagicMock, - bound_firewall, - response_update_firewall, - ): - request_mock.return_value = response_update_firewall - - firewall = bound_firewall.update( - name="New Corporate Intranet Protection", labels={} - ) - - request_mock.assert_called_with( - method="PUT", - url="/firewalls/1", - json={"name": "New Corporate Intranet Protection", "labels": {}}, - ) - - assert firewall.id == 38 - assert firewall.name == "New Corporate Intranet Protection" - - def test_delete( - self, - request_mock: mock.MagicMock, - bound_firewall, - ): - delete_success = bound_firewall.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/firewalls/1", - ) - - assert delete_success is True - - def test_set_rules( - self, - request_mock: mock.MagicMock, - bound_firewall, - response_set_rules, - ): - request_mock.return_value = response_set_rules - - actions = bound_firewall.set_rules( - [ - FirewallRule( - direction=FirewallRule.DIRECTION_IN, - protocol=FirewallRule.PROTOCOL_ICMP, - source_ips=["0.0.0.0/0", "::/0"], - description="New firewall description", - ) - ] - ) - - request_mock.assert_called_with( - method="POST", - url="/firewalls/1/actions/set_rules", - json={ - "rules": [ - { - "direction": "in", - "protocol": "icmp", - "source_ips": ["0.0.0.0/0", "::/0"], - "description": "New firewall description", - } - ] - }, - ) - - assert actions[0].id == 13 - assert actions[0].progress == 100 - - def test_apply_to_resources( - self, - request_mock: mock.MagicMock, - bound_firewall, - response_set_rules, - ): - request_mock.return_value = response_set_rules - - actions = bound_firewall.apply_to_resources( - [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))] - ) - - request_mock.assert_called_with( - method="POST", - url="/firewalls/1/actions/apply_to_resources", - json={"apply_to": [{"type": "server", "server": {"id": 5}}]}, - ) - - assert actions[0].id == 13 - assert actions[0].progress == 100 - - def test_remove_from_resources( - self, - request_mock: mock.MagicMock, - bound_firewall, - response_set_rules, - ): - request_mock.return_value = response_set_rules - - actions = bound_firewall.remove_from_resources( - [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))] - ) - - request_mock.assert_called_with( - method="POST", - url="/firewalls/1/actions/remove_from_resources", - json={"remove_from": [{"type": "server", "server": {"id": 5}}]}, - ) - - assert actions[0].id == 13 - assert actions[0].progress == 100 - class TestFirewallsClient: @pytest.fixture() @@ -421,7 +318,15 @@ def test_set_rules( direction=FirewallRule.DIRECTION_IN, protocol=FirewallRule.PROTOCOL_ICMP, source_ips=["0.0.0.0/0", "::/0"], - ) + description="Allow ICMP from everywhere", + ), + FirewallRule( + direction=FirewallRule.DIRECTION_IN, + protocol=FirewallRule.PROTOCOL_TCP, + port="80", + source_ips=["0.0.0.0/0", "::/0"], + description="Allow HTTP from everywhere", + ), ], ) @@ -434,7 +339,15 @@ def test_set_rules( "direction": "in", "protocol": "icmp", "source_ips": ["0.0.0.0/0", "::/0"], - } + "description": "Allow ICMP from everywhere", + }, + { + "direction": "in", + "protocol": "tcp", + "port": "80", + "source_ips": ["0.0.0.0/0", "::/0"], + "description": "Allow HTTP from everywhere", + }, ] }, ) diff --git a/tests/unit/floating_ips/test_client.py b/tests/unit/floating_ips/test_client.py index acd2b175..6bc991b9 100644 --- a/tests/unit/floating_ips/test_client.py +++ b/tests/unit/floating_ips/test_client.py @@ -9,152 +9,51 @@ from hcloud.locations import BoundLocation, Location from hcloud.servers import BoundServer, Server +from ..conftest import BoundModelTestCase -class TestBoundFloatingIP: - @pytest.fixture() - def bound_floating_ip(self, client: Client): - return BoundFloatingIP(client.floating_ips, data=dict(id=14)) - - def test_bound_floating_ip_init(self, floating_ip_response): - bound_floating_ip = BoundFloatingIP( - client=mock.MagicMock(), data=floating_ip_response["floating_ip"] - ) - - assert bound_floating_ip.id == 4711 - assert bound_floating_ip.description == "Web Frontend" - assert bound_floating_ip.name == "Web Frontend" - assert bound_floating_ip.ip == "131.232.99.1" - assert bound_floating_ip.type == "ipv4" - assert bound_floating_ip.protection == {"delete": False} - assert bound_floating_ip.labels == {} - assert bound_floating_ip.blocked is False - - assert isinstance(bound_floating_ip.server, BoundServer) - assert bound_floating_ip.server.id == 42 - - assert isinstance(bound_floating_ip.home_location, BoundLocation) - assert bound_floating_ip.home_location.id == 1 - assert bound_floating_ip.home_location.name == "fsn1" - assert bound_floating_ip.home_location.description == "Falkenstein DC Park 1" - assert bound_floating_ip.home_location.country == "DE" - assert bound_floating_ip.home_location.city == "Falkenstein" - assert bound_floating_ip.home_location.latitude == 50.47612 - assert bound_floating_ip.home_location.longitude == 12.370071 - - def test_update( - self, - request_mock: mock.MagicMock, - bound_floating_ip, - response_update_floating_ip, - ): - request_mock.return_value = response_update_floating_ip - - floating_ip = bound_floating_ip.update( - description="New description", name="New name" - ) - - request_mock.assert_called_with( - method="PUT", - url="/floating_ips/14", - json={"description": "New description", "name": "New name"}, - ) - - assert floating_ip.id == 4711 - assert floating_ip.description == "New description" - assert floating_ip.name == "New name" - def test_delete( - self, - request_mock: mock.MagicMock, - bound_floating_ip, - action_response, - ): - request_mock.return_value = action_response - - delete_success = bound_floating_ip.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/floating_ips/14", - ) - - assert delete_success is True +class TestBoundFloatingIP(BoundModelTestCase): + methods = [ + BoundFloatingIP.update, + BoundFloatingIP.delete, + BoundFloatingIP.change_protection, + BoundFloatingIP.change_dns_ptr, + BoundFloatingIP.assign, + BoundFloatingIP.unassign, + ] - def test_change_protection( - self, - request_mock: mock.MagicMock, - bound_floating_ip, - action_response, - ): - request_mock.return_value = action_response - - action = bound_floating_ip.change_protection(True) - - request_mock.assert_called_with( - method="POST", - url="/floating_ips/14/actions/change_protection", - json={"delete": True}, - ) - - assert action.id == 1 - assert action.progress == 0 - - @pytest.mark.parametrize( - "server", (Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))) - ) - def test_assign( - self, - request_mock: mock.MagicMock, - bound_floating_ip, - server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_floating_ip.assign(server) - - request_mock.assert_called_with( - method="POST", - url="/floating_ips/14/actions/assign", - json={"server": 1}, - ) - assert action.id == 1 - assert action.progress == 0 - - def test_unassign( - self, - request_mock: mock.MagicMock, - bound_floating_ip, - action_response, - ): - request_mock.return_value = action_response - - action = bound_floating_ip.unassign() + @pytest.fixture() + def resource_client(self, client: Client): + return client.floating_ips - request_mock.assert_called_with( - method="POST", - url="/floating_ips/14/actions/unassign", + @pytest.fixture() + def bound_model(self, resource_client, floating_ip_response): + return BoundFloatingIP( + resource_client, data=floating_ip_response["floating_ip"] ) - assert action.id == 1 - assert action.progress == 0 - - def test_change_dns_ptr( - self, - request_mock: mock.MagicMock, - bound_floating_ip, - action_response, - ): - request_mock.return_value = action_response - - action = bound_floating_ip.change_dns_ptr("1.2.3.4", "server02.example.com") - request_mock.assert_called_with( - method="POST", - url="/floating_ips/14/actions/change_dns_ptr", - json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, - ) - assert action.id == 1 - assert action.progress == 0 + def test_init(self, bound_model: BoundFloatingIP): + o = bound_model + assert o.id == 4711 + assert o.description == "Web Frontend" + assert o.name == "Web Frontend" + assert o.ip == "131.232.99.1" + assert o.type == "ipv4" + assert o.protection == {"delete": False} + assert o.labels == {} + assert o.blocked is False + + assert isinstance(o.server, BoundServer) + assert o.server.id == 42 + + assert isinstance(o.home_location, BoundLocation) + assert o.home_location.id == 1 + assert o.home_location.name == "fsn1" + assert o.home_location.description == "Falkenstein DC Park 1" + assert o.home_location.country == "DE" + assert o.home_location.city == "Falkenstein" + assert o.home_location.latitude == 50.47612 + assert o.home_location.longitude == 12.370071 class TestFloatingIPsClient: diff --git a/tests/unit/images/test_client.py b/tests/unit/images/test_client.py index bc927983..d88b36bf 100644 --- a/tests/unit/images/test_client.py +++ b/tests/unit/images/test_client.py @@ -10,13 +10,25 @@ from hcloud.images import BoundImage, Image, ImagesClient from hcloud.servers import BoundServer +from ..conftest import BoundModelTestCase + + +class TestBoundImage(BoundModelTestCase): + methods = [ + BoundImage.update, + BoundImage.delete, + BoundImage.change_protection, + ] + + @pytest.fixture() + def resource_client(self, client: Client): + return client.images -class TestBoundImage: @pytest.fixture() - def bound_image(self, client: Client): - return BoundImage(client.images, data=dict(id=14)) + def bound_model(self, resource_client): + return BoundImage(resource_client, data=dict(id=14)) - def test_bound_image_init(self, image_response): + def test_init(self, image_response): bound_image = BoundImage(client=mock.MagicMock(), data=image_response["image"]) assert bound_image.id == 4711 @@ -46,67 +58,6 @@ def test_bound_image_init(self, image_response): assert bound_image.bound_to.id == 1 assert bound_image.bound_to.complete is False - def test_update( - self, - request_mock: mock.MagicMock, - bound_image, - response_update_image, - ): - request_mock.return_value = response_update_image - - image = bound_image.update( - description="My new Image description", type="snapshot", labels={} - ) - - request_mock.assert_called_with( - method="PUT", - url="/images/14", - json={ - "description": "My new Image description", - "type": "snapshot", - "labels": {}, - }, - ) - - assert image.id == 4711 - assert image.description == "My new Image description" - - def test_delete( - self, - request_mock: mock.MagicMock, - bound_image, - action_response, - ): - request_mock.return_value = action_response - - delete_success = bound_image.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/images/14", - ) - - assert delete_success is True - - def test_change_protection( - self, - request_mock: mock.MagicMock, - bound_image, - action_response, - ): - request_mock.return_value = action_response - - action = bound_image.change_protection(True) - - request_mock.assert_called_with( - method="POST", - url="/images/14/actions/change_protection", - json={"delete": True}, - ) - - assert action.id == 1 - assert action.progress == 0 - class TestImagesClient: @pytest.fixture() diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index 94e5fb63..7ac3290c 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -21,13 +21,38 @@ from hcloud.networks import Network from hcloud.servers import Server +from ..conftest import BoundModelTestCase + + +class TestBoundLoadBalancer(BoundModelTestCase): + methods = [ + BoundLoadBalancer.update, + BoundLoadBalancer.delete, + BoundLoadBalancer.change_algorithm, + BoundLoadBalancer.change_dns_ptr, + BoundLoadBalancer.change_protection, + BoundLoadBalancer.change_type, + BoundLoadBalancer.add_service, + BoundLoadBalancer.update_service, + BoundLoadBalancer.delete_service, + BoundLoadBalancer.add_target, + BoundLoadBalancer.remove_target, + BoundLoadBalancer.attach_to_network, + BoundLoadBalancer.detach_from_network, + BoundLoadBalancer.disable_public_interface, + BoundLoadBalancer.enable_public_interface, + BoundLoadBalancer.get_metrics, + ] -class TestBoundLoadBalancer: @pytest.fixture() - def bound_load_balancer(self, client: Client): - return BoundLoadBalancer(client.load_balancers, data=dict(id=14)) + def resource_client(self, client: Client): + return client.load_balancers - def test_bound_load_balancer_init(self, response_load_balancer): + @pytest.fixture() + def bound_model(self, resource_client: LoadBalancersClient): + return BoundLoadBalancer(resource_client, data=dict(id=1)) + + def test_init(self, response_load_balancer): bound_load_balancer = BoundLoadBalancer( client=mock.MagicMock(), data=response_load_balancer["load_balancer"] ) @@ -35,51 +60,275 @@ def test_bound_load_balancer_init(self, response_load_balancer): assert bound_load_balancer.id == 4711 assert bound_load_balancer.name == "Web Frontend" + +class TestLoadBalancerslient: + @pytest.fixture() + def resource_client(self, client: Client): + return client.load_balancers + + def test_get_by_id( + self, + request_mock: mock.MagicMock, + resource_client: LoadBalancersClient, + response_load_balancer, + ): + request_mock.return_value = response_load_balancer + + bound_load_balancer = resource_client.get_by_id(1) + + request_mock.assert_called_with( + method="GET", + url="/load_balancers/1", + ) + assert bound_load_balancer._client is resource_client + assert bound_load_balancer.id == 4711 + assert bound_load_balancer.name == "Web Frontend" + assert bound_load_balancer.outgoing_traffic == 123456 + assert bound_load_balancer.ingoing_traffic == 123456 + assert bound_load_balancer.included_traffic == 654321 + + @pytest.mark.parametrize( + "params", + [ + { + "name": "load_balancer1", + "label_selector": "label1", + "page": 1, + "per_page": 10, + }, + {"name": ""}, + {}, + ], + ) + def test_get_list( + self, + request_mock: mock.MagicMock, + resource_client: LoadBalancersClient, + response_simple_load_balancers, + params, + ): + request_mock.return_value = response_simple_load_balancers + + result = resource_client.get_list(**params) + + request_mock.assert_called_with( + method="GET", + url="/load_balancers", + params=params, + ) + + bound_load_balancers = result.load_balancers + assert result.meta is not None + + assert len(bound_load_balancers) == 2 + + bound_load_balancer1 = bound_load_balancers[0] + bound_load_balancer2 = bound_load_balancers[1] + + assert bound_load_balancer1._client is resource_client + assert bound_load_balancer1.id == 4711 + assert bound_load_balancer1.name == "Web Frontend" + + assert bound_load_balancer2._client is resource_client + assert bound_load_balancer2.id == 4712 + assert bound_load_balancer2.name == "Web Frontend2" + + @pytest.mark.parametrize( + "params", [{"name": "loadbalancer1", "label_selector": "label1"}, {}] + ) + def test_get_all( + self, + request_mock: mock.MagicMock, + resource_client: LoadBalancersClient, + response_simple_load_balancers, + params, + ): + request_mock.return_value = response_simple_load_balancers + + bound_load_balancers = resource_client.get_all(**params) + + params.update({"page": 1, "per_page": 50}) + + request_mock.assert_called_with( + method="GET", + url="/load_balancers", + params=params, + ) + + assert len(bound_load_balancers) == 2 + + bound_load_balancer1 = bound_load_balancers[0] + bound_load_balancer2 = bound_load_balancers[1] + + assert bound_load_balancer1._client is resource_client + assert bound_load_balancer1.id == 4711 + assert bound_load_balancer1.name == "Web Frontend" + + assert bound_load_balancer2._client is resource_client + assert bound_load_balancer2.id == 4712 + assert bound_load_balancer2.name == "Web Frontend2" + + def test_get_by_name( + self, + request_mock: mock.MagicMock, + resource_client: LoadBalancersClient, + response_simple_load_balancers, + ): + request_mock.return_value = response_simple_load_balancers + + bound_load_balancer = resource_client.get_by_name("Web Frontend") + + params = {"name": "Web Frontend"} + + request_mock.assert_called_with( + method="GET", + url="/load_balancers", + params=params, + ) + + assert bound_load_balancer._client is resource_client + assert bound_load_balancer.id == 4711 + assert bound_load_balancer.name == "Web Frontend" + + def test_create( + self, + request_mock: mock.MagicMock, + resource_client: LoadBalancersClient, + response_create_load_balancer, + ): + request_mock.return_value = response_create_load_balancer + + response = resource_client.create( + "my-balancer", + load_balancer_type=LoadBalancerType(name="lb11"), + location=Location(id=1), + ) + + request_mock.assert_called_with( + method="POST", + url="/load_balancers", + json={"name": "my-balancer", "load_balancer_type": "lb11", "location": 1}, + ) + + bound_load_balancer = response.load_balancer + + assert bound_load_balancer._client is resource_client + assert bound_load_balancer.id == 1 + assert bound_load_balancer.name == "my-balancer" + + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) + def test_change_type_with_load_balancer_type_name( + self, + request_mock: mock.MagicMock, + resource_client: LoadBalancersClient, + load_balancer, + action_response, + ): + request_mock.return_value = action_response + + action = resource_client.change_type( + load_balancer, LoadBalancerType(name="lb11") + ) + + request_mock.assert_called_with( + method="POST", + url="/load_balancers/1/actions/change_type", + json={"load_balancer_type": "lb11"}, + ) + + assert action.id == 1 + assert action.progress == 0 + + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) + def test_change_type_with_load_balancer_type_id( + self, + request_mock: mock.MagicMock, + resource_client: LoadBalancersClient, + load_balancer, + action_response, + ): + request_mock.return_value = action_response + + action = resource_client.change_type(load_balancer, LoadBalancerType(id=1)) + + request_mock.assert_called_with( + method="POST", + url="/load_balancers/1/actions/change_type", + json={"load_balancer_type": 1}, + ) + + assert action.id == 1 + assert action.progress == 0 + + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_update( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_update_load_balancer, ): request_mock.return_value = response_update_load_balancer - load_balancer = bound_load_balancer.update(name="new-name", labels={}) + load_balancer = resource_client.update( + load_balancer, name="new-name", labels={} + ) request_mock.assert_called_with( method="PUT", - url="/load_balancers/14", + url="/load_balancers/1", json={"name": "new-name", "labels": {}}, ) assert load_balancer.id == 4711 assert load_balancer.name == "new-name" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_delete( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, action_response, ): request_mock.return_value = action_response - delete_success = bound_load_balancer.delete() + delete_success = resource_client.delete(load_balancer) request_mock.assert_called_with( method="DELETE", - url="/load_balancers/14", + url="/load_balancers/1", ) assert delete_success is True + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_get_metrics( self, request_mock: mock.MagicMock, - bound_load_balancer: BoundLoadBalancer, + resource_client: LoadBalancersClient, + load_balancer, response_get_metrics, ): request_mock.return_value = response_get_metrics - response = bound_load_balancer.get_metrics( + response = resource_client.get_metrics( + load_balancer, type=["requests_per_second"], start="2023-12-14T16:55:32+01:00", end="2023-12-14T16:55:32+01:00", @@ -87,7 +336,7 @@ def test_get_metrics( request_mock.assert_called_with( method="GET", - url="/load_balancers/14/metrics", + url="/load_balancers/1/metrics", params={ "type": "requests_per_second", "start": "2023-12-14T16:55:32+01:00", @@ -97,20 +346,25 @@ def test_get_metrics( assert "requests_per_second" in response.metrics.time_series assert len(response.metrics.time_series["requests_per_second"]["values"]) == 3 + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_add_service( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_add_service, ): request_mock.return_value = response_add_service service = LoadBalancerService(listen_port=80, protocol="http") - action = bound_load_balancer.add_service(service) + action = resource_client.add_service(load_balancer, service) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/add_service", + url="/load_balancers/1/actions/add_service", json={"protocol": "http", "listen_port": 80}, ) @@ -118,20 +372,25 @@ def test_add_service( assert action.progress == 100 assert action.command == "add_service" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_delete_service( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_delete_service, ): request_mock.return_value = response_delete_service service = LoadBalancerService(listen_port=12) - action = bound_load_balancer.delete_service(service) + action = resource_client.delete_service(load_balancer, service) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/delete_service", + url="/load_balancers/1/actions/delete_service", json={"listen_port": 12}, ) @@ -139,6 +398,10 @@ def test_delete_service( assert action.progress == 100 assert action.command == "delete_service" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) @pytest.mark.parametrize( "target,params", [ @@ -164,19 +427,20 @@ def test_delete_service( def test_add_target( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_add_target, target, params, ): request_mock.return_value = response_add_target - action = bound_load_balancer.add_target(target) + action = resource_client.add_target(load_balancer, target) params.update({"type": target.type}) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/add_target", + url="/load_balancers/1/actions/add_target", json=params, ) @@ -184,6 +448,10 @@ def test_add_target( assert action.progress == 100 assert action.command == "add_target" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) @pytest.mark.parametrize( "target,params", [ @@ -209,19 +477,20 @@ def test_add_target( def test_remove_target( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_remove_target, target, params, ): request_mock.return_value = response_remove_target - action = bound_load_balancer.remove_target(target) + action = resource_client.remove_target(load_balancer, target) params.update({"type": target.type}) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/remove_target", + url="/load_balancers/1/actions/remove_target", json=params, ) @@ -229,10 +498,15 @@ def test_remove_target( assert action.progress == 100 assert action.command == "remove_target" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_update_service( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_update_service, ): request_mock.return_value = response_update_service @@ -242,11 +516,11 @@ def test_update_service( ) service = LoadBalancerService(listen_port=12, health_check=new_health_check) - action = bound_load_balancer.update_service(service) + action = resource_client.update_service(load_balancer, service) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/update_service", + url="/load_balancers/1/actions/update_service", json={ "listen_port": 12, "health_check": { @@ -263,20 +537,25 @@ def test_update_service( assert action.progress == 100 assert action.command == "update_service" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_change_algorithm( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_change_algorithm, ): request_mock.return_value = response_change_algorithm algorithm = LoadBalancerAlgorithm(type="round_robin") - action = bound_load_balancer.change_algorithm(algorithm) + action = resource_client.change_algorithm(load_balancer, algorithm) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/change_algorithm", + url="/load_balancers/1/actions/change_algorithm", json={"type": "round_robin"}, ) @@ -284,21 +563,26 @@ def test_change_algorithm( assert action.progress == 100 assert action.command == "change_algorithm" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_change_dns_ptr( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_change_reverse_dns_entry, ): request_mock.return_value = response_change_reverse_dns_entry - action = bound_load_balancer.change_dns_ptr( - ip="1.2.3.4", dns_ptr="lb1.example.com" + action = resource_client.change_dns_ptr( + load_balancer, ip="1.2.3.4", dns_ptr="lb1.example.com" ) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/change_dns_ptr", + url="/load_balancers/1/actions/change_dns_ptr", json={"dns_ptr": "lb1.example.com", "ip": "1.2.3.4"}, ) @@ -306,19 +590,24 @@ def test_change_dns_ptr( assert action.progress == 100 assert action.command == "change_dns_ptr" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_change_protection( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_change_protection, ): request_mock.return_value = response_change_protection - action = bound_load_balancer.change_protection(delete=True) + action = resource_client.change_protection(load_balancer, delete=True) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/change_protection", + url="/load_balancers/1/actions/change_protection", json={"delete": True}, ) @@ -326,57 +615,72 @@ def test_change_protection( assert action.progress == 100 assert action.command == "change_protection" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_enable_public_interface( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_enable_public_interface, ): request_mock.return_value = response_enable_public_interface - action = bound_load_balancer.enable_public_interface() + action = resource_client.enable_public_interface(load_balancer) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/enable_public_interface", + url="/load_balancers/1/actions/enable_public_interface", ) assert action.id == 13 assert action.progress == 100 assert action.command == "enable_public_interface" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_disable_public_interface( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_disable_public_interface, ): request_mock.return_value = response_disable_public_interface - action = bound_load_balancer.disable_public_interface() + action = resource_client.disable_public_interface(load_balancer) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/disable_public_interface", + url="/load_balancers/1/actions/disable_public_interface", ) assert action.id == 13 assert action.progress == 100 assert action.command == "disable_public_interface" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_attach_to_network( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_attach_load_balancer_to_network, ): request_mock.return_value = response_attach_load_balancer_to_network - action = bound_load_balancer.attach_to_network(Network(id=1)) + action = resource_client.attach_to_network(load_balancer, Network(id=1)) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/attach_to_network", + url="/load_balancers/1/actions/attach_to_network", json={"network": 1}, ) @@ -384,19 +688,24 @@ def test_attach_to_network( assert action.progress == 100 assert action.command == "attach_to_network" + @pytest.mark.parametrize( + "load_balancer", + [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], + ) def test_detach_from_network( self, request_mock: mock.MagicMock, - bound_load_balancer, + resource_client: LoadBalancersClient, + load_balancer, response_detach_from_network, ): request_mock.return_value = response_detach_from_network - action = bound_load_balancer.detach_from_network(Network(id=1)) + action = resource_client.detach_from_network(load_balancer, Network(id=1)) request_mock.assert_called_with( method="POST", - url="/load_balancers/14/actions/detach_from_network", + url="/load_balancers/1/actions/detach_from_network", json={"network": 1}, ) @@ -404,228 +713,27 @@ def test_detach_from_network( assert action.progress == 100 assert action.command == "detach_from_network" - def test_change_type( - self, - request_mock: mock.MagicMock, - bound_load_balancer, - action_response, - ): - request_mock.return_value = action_response - - action = bound_load_balancer.change_type(LoadBalancerType(name="lb21")) - - request_mock.assert_called_with( - method="POST", - url="/load_balancers/14/actions/change_type", - json={"load_balancer_type": "lb21"}, - ) - - assert action.id == 1 - assert action.progress == 0 - - -class TestLoadBalancerslient: - @pytest.fixture() - def load_balancers_client(self, client: Client): - return LoadBalancersClient(client) - - def test_get_by_id( - self, - request_mock: mock.MagicMock, - load_balancers_client: LoadBalancersClient, - response_load_balancer, - ): - request_mock.return_value = response_load_balancer - - bound_load_balancer = load_balancers_client.get_by_id(1) - - request_mock.assert_called_with( - method="GET", - url="/load_balancers/1", - ) - assert bound_load_balancer._client is load_balancers_client - assert bound_load_balancer.id == 4711 - assert bound_load_balancer.name == "Web Frontend" - assert bound_load_balancer.outgoing_traffic == 123456 - assert bound_load_balancer.ingoing_traffic == 123456 - assert bound_load_balancer.included_traffic == 654321 - - @pytest.mark.parametrize( - "params", - [ - { - "name": "load_balancer1", - "label_selector": "label1", - "page": 1, - "per_page": 10, - }, - {"name": ""}, - {}, - ], - ) - def test_get_list( - self, - request_mock: mock.MagicMock, - load_balancers_client: LoadBalancersClient, - response_simple_load_balancers, - params, - ): - request_mock.return_value = response_simple_load_balancers - - result = load_balancers_client.get_list(**params) - - request_mock.assert_called_with( - method="GET", - url="/load_balancers", - params=params, - ) - - bound_load_balancers = result.load_balancers - assert result.meta is not None - - assert len(bound_load_balancers) == 2 - - bound_load_balancer1 = bound_load_balancers[0] - bound_load_balancer2 = bound_load_balancers[1] - - assert bound_load_balancer1._client is load_balancers_client - assert bound_load_balancer1.id == 4711 - assert bound_load_balancer1.name == "Web Frontend" - - assert bound_load_balancer2._client is load_balancers_client - assert bound_load_balancer2.id == 4712 - assert bound_load_balancer2.name == "Web Frontend2" - - @pytest.mark.parametrize( - "params", [{"name": "loadbalancer1", "label_selector": "label1"}, {}] - ) - def test_get_all( - self, - request_mock: mock.MagicMock, - load_balancers_client: LoadBalancersClient, - response_simple_load_balancers, - params, - ): - request_mock.return_value = response_simple_load_balancers - - bound_load_balancers = load_balancers_client.get_all(**params) - - params.update({"page": 1, "per_page": 50}) - - request_mock.assert_called_with( - method="GET", - url="/load_balancers", - params=params, - ) - - assert len(bound_load_balancers) == 2 - - bound_load_balancer1 = bound_load_balancers[0] - bound_load_balancer2 = bound_load_balancers[1] - - assert bound_load_balancer1._client is load_balancers_client - assert bound_load_balancer1.id == 4711 - assert bound_load_balancer1.name == "Web Frontend" - - assert bound_load_balancer2._client is load_balancers_client - assert bound_load_balancer2.id == 4712 - assert bound_load_balancer2.name == "Web Frontend2" - - def test_get_by_name( - self, - request_mock: mock.MagicMock, - load_balancers_client: LoadBalancersClient, - response_simple_load_balancers, - ): - request_mock.return_value = response_simple_load_balancers - - bound_load_balancer = load_balancers_client.get_by_name("Web Frontend") - - params = {"name": "Web Frontend"} - - request_mock.assert_called_with( - method="GET", - url="/load_balancers", - params=params, - ) - - assert bound_load_balancer._client is load_balancers_client - assert bound_load_balancer.id == 4711 - assert bound_load_balancer.name == "Web Frontend" - - def test_create( - self, - request_mock: mock.MagicMock, - load_balancers_client: LoadBalancersClient, - response_create_load_balancer, - ): - request_mock.return_value = response_create_load_balancer - - response = load_balancers_client.create( - "my-balancer", - load_balancer_type=LoadBalancerType(name="lb11"), - location=Location(id=1), - ) - - request_mock.assert_called_with( - method="POST", - url="/load_balancers", - json={"name": "my-balancer", "load_balancer_type": "lb11", "location": 1}, - ) - - bound_load_balancer = response.load_balancer - - assert bound_load_balancer._client is load_balancers_client - assert bound_load_balancer.id == 1 - assert bound_load_balancer.name == "my-balancer" - @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) - def test_change_type_with_load_balancer_type_name( - self, - request_mock: mock.MagicMock, - load_balancers_client: LoadBalancersClient, - load_balancer, - action_response, - ): - request_mock.return_value = action_response - - action = load_balancers_client.change_type( - load_balancer, LoadBalancerType(name="lb11") - ) - - request_mock.assert_called_with( - method="POST", - url="/load_balancers/1/actions/change_type", - json={"load_balancer_type": "lb11"}, - ) - - assert action.id == 1 - assert action.progress == 0 - - @pytest.mark.parametrize( - "load_balancer", - [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], - ) - def test_change_type_with_load_balancer_type_id( + def test_change_type( self, request_mock: mock.MagicMock, - load_balancers_client: LoadBalancersClient, + resource_client: LoadBalancersClient, load_balancer, action_response, ): request_mock.return_value = action_response - action = load_balancers_client.change_type( - load_balancer, LoadBalancerType(id=1) + action = resource_client.change_type( + load_balancer, LoadBalancerType(name="lb21") ) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/change_type", - json={"load_balancer_type": 1}, + json={"load_balancer_type": "lb21"}, ) assert action.id == 1 diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 5b60983b..3f162b46 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -15,13 +15,30 @@ ) from hcloud.servers import BoundServer +from ..conftest import BoundModelTestCase + + +class TestBoundNetwork(BoundModelTestCase): + methods = [ + BoundNetwork.update, + BoundNetwork.delete, + BoundNetwork.add_subnet, + BoundNetwork.delete_subnet, + BoundNetwork.add_route, + BoundNetwork.delete_route, + BoundNetwork.change_ip_range, + BoundNetwork.change_protection, + ] -class TestBoundNetwork: @pytest.fixture() - def bound_network(self, client: Client): - return BoundNetwork(client.networks, data=dict(id=14)) + def resource_client(self, client: Client): + return client.networks - def test_bound_network_init(self, network_response): + @pytest.fixture() + def bound_model(self, resource_client: NetworksClient): + return BoundNetwork(resource_client, data=dict(id=14)) + + def test_init(self, network_response): bound_network = BoundNetwork( client=mock.MagicMock(), data=network_response["network"] ) @@ -49,168 +66,6 @@ def test_bound_network_init(self, network_response): assert bound_network.routes[0].destination == "10.100.1.0/24" assert bound_network.routes[0].gateway == "10.0.1.1" - def test_update( - self, - request_mock: mock.MagicMock, - bound_network, - response_update_network, - ): - request_mock.return_value = response_update_network - - network = bound_network.update(name="new-name") - - request_mock.assert_called_with( - method="PUT", - url="/networks/14", - json={"name": "new-name"}, - ) - - assert network.id == 4711 - assert network.name == "new-name" - - def test_delete( - self, - request_mock: mock.MagicMock, - bound_network, - action_response, - ): - request_mock.return_value = action_response - - delete_success = bound_network.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/networks/14", - ) - - assert delete_success is True - - def test_change_protection( - self, - request_mock: mock.MagicMock, - bound_network, - action_response, - ): - request_mock.return_value = action_response - - action = bound_network.change_protection(True) - - request_mock.assert_called_with( - method="POST", - url="/networks/14/actions/change_protection", - json={"delete": True}, - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_add_subnet( - self, - request_mock: mock.MagicMock, - bound_network, - action_response, - ): - request_mock.return_value = action_response - - subnet = NetworkSubnet( - type=NetworkSubnet.TYPE_CLOUD, - ip_range="10.0.1.0/24", - network_zone="eu-central", - ) - action = bound_network.add_subnet(subnet) - - request_mock.assert_called_with( - method="POST", - url="/networks/14/actions/add_subnet", - json={ - "type": NetworkSubnet.TYPE_CLOUD, - "ip_range": "10.0.1.0/24", - "network_zone": "eu-central", - }, - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_delete_subnet( - self, - request_mock: mock.MagicMock, - bound_network, - action_response, - ): - request_mock.return_value = action_response - - subnet = NetworkSubnet(ip_range="10.0.1.0/24") - action = bound_network.delete_subnet(subnet) - - request_mock.assert_called_with( - method="POST", - url="/networks/14/actions/delete_subnet", - json={"ip_range": "10.0.1.0/24"}, - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_add_route( - self, - request_mock: mock.MagicMock, - bound_network, - action_response, - ): - request_mock.return_value = action_response - - route = NetworkRoute(destination="10.100.1.0/24", gateway="10.0.1.1") - action = bound_network.add_route(route) - - request_mock.assert_called_with( - method="POST", - url="/networks/14/actions/add_route", - json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_delete_route( - self, - request_mock: mock.MagicMock, - bound_network, - action_response, - ): - request_mock.return_value = action_response - - route = NetworkRoute(destination="10.100.1.0/24", gateway="10.0.1.1") - action = bound_network.delete_route(route) - - request_mock.assert_called_with( - method="POST", - url="/networks/14/actions/delete_route", - json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_change_ip( - self, - request_mock: mock.MagicMock, - bound_network, - action_response, - ): - request_mock.return_value = action_response - - action = bound_network.change_ip_range("10.0.0.0/12") - - request_mock.assert_called_with( - method="POST", - url="/networks/14/actions/change_ip_range", - json={"ip_range": "10.0.0.0/12"}, - ) - - assert action.id == 1 - assert action.progress == 0 - class TestNetworksClient: @pytest.fixture() diff --git a/tests/unit/placement_groups/test_client.py b/tests/unit/placement_groups/test_client.py index fc1f2d57..0c35d41b 100644 --- a/tests/unit/placement_groups/test_client.py +++ b/tests/unit/placement_groups/test_client.py @@ -5,10 +5,15 @@ import pytest from hcloud import Client -from hcloud.placement_groups import BoundPlacementGroup, PlacementGroupsClient +from hcloud.placement_groups import ( + BoundPlacementGroup, + PlacementGroupsClient, +) +from ..conftest import BoundModelTestCase -def check_variables(placement_group, expected): + +def check_variables(placement_group: BoundPlacementGroup, expected): assert placement_group.id == expected["id"] assert placement_group.name == expected["name"] assert placement_group.labels == expected["labels"] @@ -16,12 +21,21 @@ def check_variables(placement_group, expected): assert placement_group.type == expected["type"] -class TestBoundPlacementGroup: +class TestBoundPlacementGroup(BoundModelTestCase): + methods = [ + BoundPlacementGroup.update, + BoundPlacementGroup.delete, + ] + @pytest.fixture() - def bound_placement_group(self, client: Client): - return BoundPlacementGroup(client.placement_groups, data=dict(id=897)) + def resource_client(self, client: Client): + return client.placement_groups - def test_bound_placement_group_init(self, placement_group_response): + @pytest.fixture() + def bound_model(self, resource_client: PlacementGroupsClient): + return BoundPlacementGroup(resource_client, data=dict(id=897)) + + def test_init(self, placement_group_response): bound_placement_group = BoundPlacementGroup( client=mock.MagicMock(), data=placement_group_response["placement_group"] ) @@ -30,61 +44,25 @@ def test_bound_placement_group_init(self, placement_group_response): bound_placement_group, placement_group_response["placement_group"] ) - def test_update( - self, - request_mock: mock.MagicMock, - bound_placement_group, - placement_group_response, - ): - request_mock.return_value = placement_group_response - - placement_group = bound_placement_group.update( - name=placement_group_response["placement_group"]["name"], - labels=placement_group_response["placement_group"]["labels"], - ) - - request_mock.assert_called_with( - method="PUT", - url="/placement_groups/{placement_group_id}".format( - placement_group_id=placement_group_response["placement_group"]["id"] - ), - json={ - "labels": placement_group_response["placement_group"]["labels"], - "name": placement_group_response["placement_group"]["name"], - }, - ) - - check_variables(placement_group, placement_group_response["placement_group"]) - - def test_delete( - self, - request_mock: mock.MagicMock, - bound_placement_group, - ): - delete_success = bound_placement_group.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/placement_groups/897", - ) - - assert delete_success is True - class TestPlacementGroupsClient: @pytest.fixture() - def placement_groups_client(self, client: Client): - return PlacementGroupsClient(client) + def resource_client(self, client: Client): + return client.placement_groups + + @pytest.fixture() + def bound_model(self, resource_client: PlacementGroupsClient): + return BoundPlacementGroup(resource_client, data=dict(id=897)) def test_get_by_id( self, request_mock: mock.MagicMock, - placement_groups_client: PlacementGroupsClient, + resource_client: PlacementGroupsClient, placement_group_response, ): request_mock.return_value = placement_group_response - placement_group = placement_groups_client.get_by_id( + placement_group = resource_client.get_by_id( placement_group_response["placement_group"]["id"] ) @@ -95,7 +73,7 @@ def test_get_by_id( ), ) - assert placement_group._client is placement_groups_client + assert placement_group._client is resource_client check_variables(placement_group, placement_group_response["placement_group"]) @@ -116,13 +94,13 @@ def test_get_by_id( def test_get_list( self, request_mock: mock.MagicMock, - placement_groups_client: PlacementGroupsClient, + resource_client: PlacementGroupsClient, two_placement_groups_response, params, ): request_mock.return_value = two_placement_groups_response - result = placement_groups_client.get_list(**params) + result = resource_client.get_list(**params) request_mock.assert_called_with( method="GET", @@ -140,7 +118,7 @@ def test_get_list( for placement_group, expected in zip( placement_groups, two_placement_groups_response["placement_groups"] ): - assert placement_group._client is placement_groups_client + assert placement_group._client is resource_client check_variables(placement_group, expected) @@ -158,13 +136,13 @@ def test_get_list( def test_get_all( self, request_mock: mock.MagicMock, - placement_groups_client: PlacementGroupsClient, + resource_client: PlacementGroupsClient, two_placement_groups_response, params, ): request_mock.return_value = two_placement_groups_response - placement_groups = placement_groups_client.get_all(**params) + placement_groups = resource_client.get_all(**params) params.update({"page": 1, "per_page": 50}) @@ -181,19 +159,19 @@ def test_get_all( for placement_group, expected in zip( placement_groups, two_placement_groups_response["placement_groups"] ): - assert placement_group._client is placement_groups_client + assert placement_group._client is resource_client check_variables(placement_group, expected) def test_get_by_name( self, request_mock: mock.MagicMock, - placement_groups_client: PlacementGroupsClient, + resource_client: PlacementGroupsClient, one_placement_group_response, ): request_mock.return_value = one_placement_group_response - placement_group = placement_groups_client.get_by_name( + placement_group = resource_client.get_by_name( one_placement_group_response["placement_groups"][0]["name"] ) @@ -212,12 +190,12 @@ def test_get_by_name( def test_create( self, request_mock: mock.MagicMock, - placement_groups_client: PlacementGroupsClient, + resource_client: PlacementGroupsClient, response_create_placement_group, ): request_mock.return_value = response_create_placement_group - response = placement_groups_client.create( + response = resource_client.create( name=response_create_placement_group["placement_group"]["name"], type=response_create_placement_group["placement_group"]["type"], labels=response_create_placement_group["placement_group"]["labels"], @@ -237,7 +215,48 @@ def test_create( bound_placement_group = response.placement_group - assert bound_placement_group._client is placement_groups_client + assert bound_placement_group._client is resource_client check_variables( bound_placement_group, response_create_placement_group["placement_group"] ) + + def test_update( + self, + request_mock: mock.MagicMock, + resource_client: PlacementGroupsClient, + bound_model, + placement_group_response, + ): + request_mock.return_value = placement_group_response + + placement_group = resource_client.update( + bound_model, + name=placement_group_response["placement_group"]["name"], + labels=placement_group_response["placement_group"]["labels"], + ) + + request_mock.assert_called_with( + method="PUT", + url="/placement_groups/897", + json={ + "labels": placement_group_response["placement_group"]["labels"], + "name": placement_group_response["placement_group"]["name"], + }, + ) + + check_variables(placement_group, placement_group_response["placement_group"]) + + def test_delete( + self, + request_mock: mock.MagicMock, + resource_client: PlacementGroupsClient, + bound_model, + ): + delete_success = resource_client.delete(bound_model) + + request_mock.assert_called_with( + method="DELETE", + url="/placement_groups/897", + ) + + assert delete_success is True diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py index ee3f5b4d..8886a01e 100644 --- a/tests/unit/primary_ips/test_client.py +++ b/tests/unit/primary_ips/test_client.py @@ -8,13 +8,28 @@ from hcloud.datacenters import BoundDatacenter, Datacenter from hcloud.primary_ips import BoundPrimaryIP, PrimaryIP, PrimaryIPsClient +from ..conftest import BoundModelTestCase + + +class TestBoundPrimaryIP(BoundModelTestCase): + methods = [ + BoundPrimaryIP.update, + BoundPrimaryIP.delete, + BoundPrimaryIP.change_dns_ptr, + BoundPrimaryIP.change_protection, + BoundPrimaryIP.assign, + BoundPrimaryIP.unassign, + ] + + @pytest.fixture() + def resource_client(self, client: Client): + return client.primary_ips -class TestBoundPrimaryIP: @pytest.fixture() - def bound_primary_ip(self, client: Client): - return BoundPrimaryIP(client.primary_ips, data=dict(id=14)) + def bound_model(self, resource_client: PrimaryIPsClient): + return BoundPrimaryIP(resource_client, data=dict(id=14)) - def test_bound_primary_ip_init(self, primary_ip_response): + def test_init(self, primary_ip_response): bound_primary_ip = BoundPrimaryIP( client=mock.MagicMock(), data=primary_ip_response["primary_ip"] ) @@ -39,114 +54,6 @@ def test_bound_primary_ip_init(self, primary_ip_response): assert bound_primary_ip.datacenter.location.latitude == 50.47612 assert bound_primary_ip.datacenter.location.longitude == 12.370071 - def test_update( - self, - request_mock: mock.MagicMock, - bound_primary_ip, - response_update_primary_ip, - ): - request_mock.return_value = response_update_primary_ip - - primary_ip = bound_primary_ip.update(auto_delete=True, name="my-resource") - - request_mock.assert_called_with( - method="PUT", - url="/primary_ips/14", - json={"auto_delete": True, "name": "my-resource"}, - ) - - assert primary_ip.id == 42 - assert primary_ip.auto_delete is True - - def test_delete( - self, - request_mock: mock.MagicMock, - bound_primary_ip, - action_response, - ): - request_mock.return_value = action_response - - delete_success = bound_primary_ip.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/primary_ips/14", - ) - - assert delete_success is True - - def test_change_protection( - self, - request_mock: mock.MagicMock, - bound_primary_ip, - action_response, - ): - request_mock.return_value = action_response - - action = bound_primary_ip.change_protection(True) - - request_mock.assert_called_with( - method="POST", - url="/primary_ips/14/actions/change_protection", - json={"delete": True}, - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_assign( - self, - request_mock: mock.MagicMock, - bound_primary_ip, - action_response, - ): - request_mock.return_value = action_response - - action = bound_primary_ip.assign(assignee_id=12, assignee_type="server") - - request_mock.assert_called_with( - method="POST", - url="/primary_ips/14/actions/assign", - json={"assignee_id": 12, "assignee_type": "server"}, - ) - assert action.id == 1 - assert action.progress == 0 - - def test_unassign( - self, - request_mock: mock.MagicMock, - bound_primary_ip, - action_response, - ): - request_mock.return_value = action_response - - action = bound_primary_ip.unassign() - - request_mock.assert_called_with( - method="POST", - url="/primary_ips/14/actions/unassign", - ) - assert action.id == 1 - assert action.progress == 0 - - def test_change_dns_ptr( - self, - request_mock: mock.MagicMock, - bound_primary_ip, - action_response, - ): - request_mock.return_value = action_response - - action = bound_primary_ip.change_dns_ptr("1.2.3.4", "server02.example.com") - - request_mock.assert_called_with( - method="POST", - url="/primary_ips/14/actions/change_dns_ptr", - json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, - ) - assert action.id == 1 - assert action.progress == 0 - class TestPrimaryIPsClient: @pytest.fixture() diff --git a/tests/unit/servers/test_client.py b/tests/unit/servers/test_client.py index 423dc29b..f3cb490e 100644 --- a/tests/unit/servers/test_client.py +++ b/tests/unit/servers/test_client.py @@ -27,630 +27,135 @@ ) from hcloud.volumes import BoundVolume, Volume +from ..conftest import BoundModelTestCase + + +class TestBoundServer(BoundModelTestCase): + methods = [ + BoundServer.update, + BoundServer.delete, + BoundServer.add_to_placement_group, + BoundServer.remove_from_placement_group, + BoundServer.attach_iso, + BoundServer.detach_iso, + BoundServer.attach_to_network, + BoundServer.detach_from_network, + BoundServer.change_alias_ips, + BoundServer.change_dns_ptr, + BoundServer.change_protection, + BoundServer.change_type, + BoundServer.create_image, + BoundServer.disable_backup, + BoundServer.enable_backup, + BoundServer.disable_rescue, + BoundServer.enable_rescue, + BoundServer.get_metrics, + BoundServer.power_off, + BoundServer.power_on, + BoundServer.reboot, + BoundServer.rebuild, + BoundServer.shutdown, + BoundServer.reset, + BoundServer.request_console, + BoundServer.reset_password, + ] -class TestBoundServer: @pytest.fixture() - def bound_server(self, client: Client): - return BoundServer(client.servers, data=dict(id=14)) + def resource_client(self, client: Client): + return client.servers - def test_bound_server_init(self, response_full_server): - bound_server = BoundServer( - client=mock.MagicMock(), data=response_full_server["server"] - ) - - assert bound_server.id == 42 - assert bound_server.name == "my-server" - assert bound_server.primary_disk_size == 20 - assert isinstance(bound_server.public_net, PublicNetwork) - assert isinstance(bound_server.public_net.ipv4, IPv4Address) - assert bound_server.public_net.ipv4.ip == "1.2.3.4" - assert bound_server.public_net.ipv4.blocked is False - assert bound_server.public_net.ipv4.dns_ptr == "server01.example.com" - - assert isinstance(bound_server.public_net.ipv6, IPv6Network) - assert bound_server.public_net.ipv6.ip == "2001:db8::/64" - assert bound_server.public_net.ipv6.blocked is False - assert bound_server.public_net.ipv6.network == "2001:db8::" - assert bound_server.public_net.ipv6.network_mask == "64" - - assert isinstance(bound_server.public_net.firewalls, list) - assert isinstance(bound_server.public_net.firewalls[0], PublicNetworkFirewall) - firewall = bound_server.public_net.firewalls[0] - assert isinstance(firewall.firewall, BoundFirewall) - assert bound_server.public_net.ipv6.blocked is False - assert firewall.status == PublicNetworkFirewall.STATUS_APPLIED - - assert isinstance(bound_server.public_net.floating_ips[0], BoundFloatingIP) - assert bound_server.public_net.floating_ips[0].id == 478 - assert bound_server.public_net.floating_ips[0].complete is False - - assert isinstance(bound_server.datacenter, BoundDatacenter) - assert ( - bound_server.datacenter._client == bound_server._client._parent.datacenters - ) - assert bound_server.datacenter.id == 1 - assert bound_server.datacenter.complete is True - - assert isinstance(bound_server.server_type, BoundServerType) - assert ( - bound_server.server_type._client - == bound_server._client._parent.server_types - ) - assert bound_server.server_type.id == 1 - assert bound_server.server_type.complete is True - - assert len(bound_server.volumes) == 2 - assert isinstance(bound_server.volumes[0], BoundVolume) - assert bound_server.volumes[0]._client == bound_server._client._parent.volumes - assert bound_server.volumes[0].id == 1 - assert bound_server.volumes[0].complete is False - - assert isinstance(bound_server.volumes[1], BoundVolume) - assert bound_server.volumes[1]._client == bound_server._client._parent.volumes - assert bound_server.volumes[1].id == 2 - assert bound_server.volumes[1].complete is False - - assert isinstance(bound_server.image, BoundImage) - assert bound_server.image._client == bound_server._client._parent.images - assert bound_server.image.id == 4711 - assert bound_server.image.name == "ubuntu-20.04" - assert bound_server.image.complete is True - - assert isinstance(bound_server.iso, BoundIso) - assert bound_server.iso._client == bound_server._client._parent.isos - assert bound_server.iso.id == 4711 - assert bound_server.iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" - assert bound_server.iso.complete is True - - assert len(bound_server.private_net) == 1 - assert isinstance(bound_server.private_net[0], PrivateNet) - assert ( - bound_server.private_net[0].network._client - == bound_server._client._parent.networks - ) - assert bound_server.private_net[0].ip == "10.1.1.5" - assert bound_server.private_net[0].mac_address == "86:00:ff:2a:7d:e1" - assert len(bound_server.private_net[0].alias_ips) == 1 - assert bound_server.private_net[0].alias_ips[0] == "10.1.1.8" - - assert isinstance(bound_server.placement_group, BoundPlacementGroup) - assert ( - bound_server.placement_group._client - == bound_server._client._parent.placement_groups - ) - assert bound_server.placement_group.id == 897 - assert bound_server.placement_group.name == "my Placement Group" - assert bound_server.placement_group.complete is True - - def test_update( - self, - request_mock: mock.MagicMock, - bound_server, - response_update_server, - ): - request_mock.return_value = response_update_server - - server = bound_server.update(name="new-name", labels={}) - - request_mock.assert_called_with( - method="PUT", - url="/servers/14", - json={"name": "new-name", "labels": {}}, - ) - - assert server.id == 14 - assert server.name == "new-name" - - def test_delete( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/servers/14", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_get_metrics( - self, - request_mock: mock.MagicMock, - bound_server: BoundServer, - response_get_metrics, - ): - request_mock.return_value = response_get_metrics - - response = bound_server.get_metrics( - type=["cpu", "disk"], - start="2023-12-14T17:40:00+01:00", - end="2023-12-14T17:50:00+01:00", - ) - - request_mock.assert_called_with( - method="GET", - url="/servers/14/metrics", - params={ - "type": "cpu,disk", - "start": "2023-12-14T17:40:00+01:00", - "end": "2023-12-14T17:50:00+01:00", - }, - ) - - assert "cpu" in response.metrics.time_series - assert "disk.0.iops.read" in response.metrics.time_series - assert len(response.metrics.time_series["disk.0.iops.read"]["values"]) == 3 - - def test_power_off( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.power_off() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/poweroff", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_power_on( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.power_on() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/poweron", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_reboot( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.reboot() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/reboot", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_reset( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.reset() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/reset", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_shutdown( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.shutdown() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/shutdown", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_reset_password( - self, - request_mock: mock.MagicMock, - bound_server, - response_server_reset_password, - ): - request_mock.return_value = response_server_reset_password - - response = bound_server.reset_password() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/reset_password", - ) - - assert response.action.id == 1 - assert response.action.progress == 0 - assert response.root_password == "YItygq1v3GYjjMomLaKc" - - def test_change_type( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.change_type(ServerType(name="cx11"), upgrade_disk=True) - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/change_type", - json={"server_type": "cx11", "upgrade_disk": True}, - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_enable_rescue( - self, - request_mock: mock.MagicMock, - bound_server, - response_server_enable_rescue, - ): - request_mock.return_value = response_server_enable_rescue - - response = bound_server.enable_rescue(type="linux64") - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/enable_rescue", - json={"type": "linux64"}, - ) - - assert response.action.id == 1 - assert response.action.progress == 0 - assert response.root_password == "YItygq1v3GYjjMomLaKc" - - def test_disable_rescue( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.disable_rescue() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/disable_rescue", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_create_image( - self, - request_mock: mock.MagicMock, - bound_server, - response_server_create_image, - ): - request_mock.return_value = response_server_create_image - - response = bound_server.create_image(description="my image", type="snapshot") - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/create_image", - json={"description": "my image", "type": "snapshot"}, - ) - - assert response.action.id == 1 - assert response.action.progress == 0 - assert response.image.description == "my image" - - def test_rebuild( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - response = bound_server.rebuild( - Image(name="ubuntu-20.04"), - return_response=True, - ) - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/rebuild", - json={"image": "ubuntu-20.04"}, - ) - - assert response.action.id == 1 - assert response.action.progress == 0 - assert response.root_password is None or isinstance(response.root_password, str) - - def test_enable_backup( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.enable_backup() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/enable_backup", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_disable_backup( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.disable_backup() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/disable_backup", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_attach_iso( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.attach_iso(Iso(name="FreeBSD-11.0-RELEASE-amd64-dvd1")) - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/attach_iso", - json={"iso": "FreeBSD-11.0-RELEASE-amd64-dvd1"}, - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_detach_iso( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.detach_iso() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/detach_iso", - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_change_dns_ptr( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.change_dns_ptr("1.2.3.4", "example.com") - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/change_dns_ptr", - json={"ip": "1.2.3.4", "dns_ptr": "example.com"}, - ) - - assert action.id == 1 - assert action.progress == 0 - - def test_change_protection( - self, - request_mock: mock.MagicMock, - bound_server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_server.change_protection(True, True) + @pytest.fixture() + def bound_model(self, resource_client: ServersClient): + return BoundServer(resource_client, data=dict(id=14)) - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/change_protection", - json={"delete": True, "rebuild": True}, + def test_init(self, response_full_server): + bound_server = BoundServer( + client=mock.MagicMock(), data=response_full_server["server"] ) - assert action.id == 1 - assert action.progress == 0 + assert bound_server.id == 42 + assert bound_server.name == "my-server" + assert bound_server.primary_disk_size == 20 + assert isinstance(bound_server.public_net, PublicNetwork) + assert isinstance(bound_server.public_net.ipv4, IPv4Address) + assert bound_server.public_net.ipv4.ip == "1.2.3.4" + assert bound_server.public_net.ipv4.blocked is False + assert bound_server.public_net.ipv4.dns_ptr == "server01.example.com" - def test_request_console( - self, - request_mock: mock.MagicMock, - bound_server, - response_server_request_console, - ): - request_mock.return_value = response_server_request_console + assert isinstance(bound_server.public_net.ipv6, IPv6Network) + assert bound_server.public_net.ipv6.ip == "2001:db8::/64" + assert bound_server.public_net.ipv6.blocked is False + assert bound_server.public_net.ipv6.network == "2001:db8::" + assert bound_server.public_net.ipv6.network_mask == "64" - response = bound_server.request_console() + assert isinstance(bound_server.public_net.firewalls, list) + assert isinstance(bound_server.public_net.firewalls[0], PublicNetworkFirewall) + firewall = bound_server.public_net.firewalls[0] + assert isinstance(firewall.firewall, BoundFirewall) + assert bound_server.public_net.ipv6.blocked is False + assert firewall.status == PublicNetworkFirewall.STATUS_APPLIED - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/request_console", - ) + assert isinstance(bound_server.public_net.floating_ips[0], BoundFloatingIP) + assert bound_server.public_net.floating_ips[0].id == 478 + assert bound_server.public_net.floating_ips[0].complete is False - assert response.action.id == 1 - assert response.action.progress == 0 + assert isinstance(bound_server.datacenter, BoundDatacenter) assert ( - response.wss_url - == "wss://console.hetzner.cloud/?server_id=1&token=3db32d15-af2f-459c-8bf8-dee1fd05f49c" - ) - assert response.password == "9MQaTg2VAGI0FIpc10k3UpRXcHj2wQ6x" - - @pytest.mark.parametrize( - "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] - ) - def test_attach_to_network( - self, - request_mock: mock.MagicMock, - bound_server, - network, - response_attach_to_network, - ): - request_mock.return_value = response_attach_to_network - - action = bound_server.attach_to_network( - network, "10.0.1.1", ["10.0.1.2", "10.0.1.3"] - ) - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/attach_to_network", - json={ - "network": 4711, - "ip": "10.0.1.1", - "alias_ips": ["10.0.1.2", "10.0.1.3"], - }, + bound_server.datacenter._client == bound_server._client._parent.datacenters ) + assert bound_server.datacenter.id == 1 + assert bound_server.datacenter.complete is True - assert action.id == 1 - assert action.progress == 0 - assert action.command == "attach_to_network" - - @pytest.mark.parametrize( - "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] - ) - def test_detach_from_network( - self, - request_mock: mock.MagicMock, - bound_server, - network, - response_detach_from_network, - ): - request_mock.return_value = response_detach_from_network - - action = bound_server.detach_from_network(network) - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/detach_from_network", - json={"network": 4711}, + assert isinstance(bound_server.server_type, BoundServerType) + assert ( + bound_server.server_type._client + == bound_server._client._parent.server_types ) + assert bound_server.server_type.id == 1 + assert bound_server.server_type.complete is True - assert action.id == 1 - assert action.progress == 0 - assert action.command == "detach_from_network" - - @pytest.mark.parametrize( - "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] - ) - def test_change_alias_ips( - self, - request_mock: mock.MagicMock, - bound_server, - network, - response_change_alias_ips, - ): - request_mock.return_value = response_change_alias_ips - - action = bound_server.change_alias_ips(network, ["10.0.1.2", "10.0.1.3"]) - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/change_alias_ips", - json={"network": 4711, "alias_ips": ["10.0.1.2", "10.0.1.3"]}, - ) + assert len(bound_server.volumes) == 2 + assert isinstance(bound_server.volumes[0], BoundVolume) + assert bound_server.volumes[0]._client == bound_server._client._parent.volumes + assert bound_server.volumes[0].id == 1 + assert bound_server.volumes[0].complete is False - assert action.id == 1 - assert action.progress == 0 - assert action.command == "change_alias_ips" + assert isinstance(bound_server.volumes[1], BoundVolume) + assert bound_server.volumes[1]._client == bound_server._client._parent.volumes + assert bound_server.volumes[1].id == 2 + assert bound_server.volumes[1].complete is False - @pytest.mark.parametrize( - "placement_group", - [PlacementGroup(id=897), BoundPlacementGroup(mock.MagicMock, dict(id=897))], - ) - def test_add_to_placement_group( - self, - request_mock: mock.MagicMock, - bound_server, - placement_group, - response_add_to_placement_group, - ): - request_mock.return_value = response_add_to_placement_group + assert isinstance(bound_server.image, BoundImage) + assert bound_server.image._client == bound_server._client._parent.images + assert bound_server.image.id == 4711 + assert bound_server.image.name == "ubuntu-20.04" + assert bound_server.image.complete is True - action = bound_server.add_to_placement_group(placement_group) + assert isinstance(bound_server.iso, BoundIso) + assert bound_server.iso._client == bound_server._client._parent.isos + assert bound_server.iso.id == 4711 + assert bound_server.iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" + assert bound_server.iso.complete is True - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/add_to_placement_group", - json={"placement_group": 897}, + assert len(bound_server.private_net) == 1 + assert isinstance(bound_server.private_net[0], PrivateNet) + assert ( + bound_server.private_net[0].network._client + == bound_server._client._parent.networks ) + assert bound_server.private_net[0].ip == "10.1.1.5" + assert bound_server.private_net[0].mac_address == "86:00:ff:2a:7d:e1" + assert len(bound_server.private_net[0].alias_ips) == 1 + assert bound_server.private_net[0].alias_ips[0] == "10.1.1.8" - assert action.id == 13 - assert action.progress == 0 - assert action.command == "add_to_placement_group" - - def test_remove_from_placement_group( - self, - request_mock: mock.MagicMock, - bound_server, - response_remove_from_placement_group, - ): - request_mock.return_value = response_remove_from_placement_group - - action = bound_server.remove_from_placement_group() - - request_mock.assert_called_with( - method="POST", - url="/servers/14/actions/remove_from_placement_group", + assert isinstance(bound_server.placement_group, BoundPlacementGroup) + assert ( + bound_server.placement_group._client + == bound_server._client._parent.placement_groups ) - - assert action.id == 13 - assert action.progress == 100 - assert action.command == "remove_from_placement_group" + assert bound_server.placement_group.id == 897 + assert bound_server.placement_group.name == "my Placement Group" + assert bound_server.placement_group.complete is True class TestServersClient: @@ -1640,3 +1145,84 @@ def test_change_alias_ips( assert action.id == 1 assert action.progress == 0 assert action.command == "change_alias_ips" + + @pytest.mark.parametrize( + "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] + ) + @pytest.mark.parametrize( + "placement_group", + [PlacementGroup(id=897), BoundPlacementGroup(mock.MagicMock, dict(id=897))], + ) + def test_add_to_placement_group( + self, + request_mock: mock.MagicMock, + servers_client: ServersClient, + server, + placement_group, + response_add_to_placement_group, + ): + request_mock.return_value = response_add_to_placement_group + + action = servers_client.add_to_placement_group(server, placement_group) + + request_mock.assert_called_with( + method="POST", + url="/servers/1/actions/add_to_placement_group", + json={"placement_group": 897}, + ) + + assert action.id == 13 + + @pytest.mark.parametrize( + "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] + ) + def test_remove_from_placement_group( + self, + request_mock: mock.MagicMock, + servers_client: ServersClient, + server, + response_remove_from_placement_group, + ): + request_mock.return_value = response_remove_from_placement_group + + action = servers_client.remove_from_placement_group(server) + + request_mock.assert_called_with( + method="POST", + url="/servers/1/actions/remove_from_placement_group", + ) + + assert action.id == 13 + + @pytest.mark.parametrize( + "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] + ) + def test_get_metrics( + self, + request_mock: mock.MagicMock, + servers_client: ServersClient, + server, + response_get_metrics, + ): + request_mock.return_value = response_get_metrics + + response = servers_client.get_metrics( + server, + type=["cpu", "disk"], + start="2023-12-14T17:40:00+01:00", + end="2023-12-14T17:50:00+01:00", + ) + + request_mock.assert_called_with( + method="GET", + url="/servers/1/metrics", + params={ + "type": "cpu,disk", + "start": "2023-12-14T17:40:00+01:00", + "end": "2023-12-14T17:50:00+01:00", + }, + ) + + assert "cpu" in response.metrics.time_series + assert "disk.0.iops.read" in response.metrics.time_series + assert len(response.metrics.time_series["disk.0.iops.read"]["values"]) == 3 diff --git a/tests/unit/ssh_keys/test_client.py b/tests/unit/ssh_keys/test_client.py index 31e58e71..bed07b54 100644 --- a/tests/unit/ssh_keys/test_client.py +++ b/tests/unit/ssh_keys/test_client.py @@ -7,13 +7,24 @@ from hcloud import Client from hcloud.ssh_keys import BoundSSHKey, SSHKey, SSHKeysClient +from ..conftest import BoundModelTestCase + + +class TestBoundSSHKey(BoundModelTestCase): + methods = [ + BoundSSHKey.update, + BoundSSHKey.delete, + ] -class TestBoundSSHKey: @pytest.fixture() - def bound_ssh_key(self, client: Client): - return BoundSSHKey(client.ssh_keys, data=dict(id=14)) + def resource_client(self, client: Client) -> SSHKeysClient: + return client.ssh_keys - def test_bound_ssh_key_init(self, ssh_key_response): + @pytest.fixture() + def bound_model(self, resource_client: SSHKeysClient) -> BoundSSHKey: + return BoundSSHKey(resource_client, data=dict(id=14)) + + def test_init(self, ssh_key_response): bound_ssh_key = BoundSSHKey( client=mock.MagicMock(), data=ssh_key_response["ssh_key"] ) @@ -26,42 +37,6 @@ def test_bound_ssh_key_init(self, ssh_key_response): ) assert bound_ssh_key.public_key == "ssh-rsa AAAjjk76kgf...Xt" - def test_update( - self, - request_mock: mock.MagicMock, - bound_ssh_key, - response_update_ssh_key, - ): - request_mock.return_value = response_update_ssh_key - - ssh_key = bound_ssh_key.update(name="New name") - - request_mock.assert_called_with( - method="PUT", - url="/ssh_keys/14", - json={"name": "New name"}, - ) - - assert ssh_key.id == 2323 - assert ssh_key.name == "New name" - - def test_delete( - self, - request_mock: mock.MagicMock, - bound_ssh_key, - action_response, - ): - request_mock.return_value = action_response - - delete_success = bound_ssh_key.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/ssh_keys/14", - ) - - assert delete_success is True - class TestSSHKeysClient: @pytest.fixture() diff --git a/tests/unit/volumes/test_client.py b/tests/unit/volumes/test_client.py index 9a1d0b20..dd86f22b 100644 --- a/tests/unit/volumes/test_client.py +++ b/tests/unit/volumes/test_client.py @@ -10,11 +10,26 @@ from hcloud.servers import BoundServer, Server from hcloud.volumes import BoundVolume, Volume, VolumesClient +from ..conftest import BoundModelTestCase + + +class TestBoundVolume(BoundModelTestCase): + methods = [ + BoundVolume.update, + BoundVolume.delete, + BoundVolume.change_protection, + BoundVolume.attach, + BoundVolume.detach, + BoundVolume.resize, + ] + + @pytest.fixture() + def resource_client(self, client: Client): + return client.volumes -class TestBoundVolume: @pytest.fixture() - def bound_volume(self, client: Client): - return BoundVolume(client.volumes, data=dict(id=14)) + def bound_model(self, resource_client): + return BoundVolume(resource_client, data=dict(id=14)) def test_bound_volume_init(self, volume_response): bound_volume = BoundVolume( @@ -41,140 +56,6 @@ def test_bound_volume_init(self, volume_response): assert bound_volume.location.latitude == 50.47612 assert bound_volume.location.longitude == 12.370071 - def test_update( - self, - request_mock: mock.MagicMock, - bound_volume, - response_update_volume, - ): - request_mock.return_value = response_update_volume - - volume = bound_volume.update(name="new-name") - - request_mock.assert_called_with( - method="PUT", - url="/volumes/14", - json={"name": "new-name"}, - ) - - assert volume.id == 4711 - assert volume.name == "new-name" - - def test_delete( - self, - request_mock: mock.MagicMock, - bound_volume, - action_response, - ): - request_mock.return_value = action_response - - delete_success = bound_volume.delete() - - request_mock.assert_called_with( - method="DELETE", - url="/volumes/14", - ) - - assert delete_success is True - - def test_change_protection( - self, - request_mock: mock.MagicMock, - bound_volume, - action_response, - ): - request_mock.return_value = action_response - - action = bound_volume.change_protection(True) - - request_mock.assert_called_with( - method="POST", - url="/volumes/14/actions/change_protection", - json={"delete": True}, - ) - - assert action.id == 1 - assert action.progress == 0 - - @pytest.mark.parametrize( - "server", (Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))) - ) - def test_attach( - self, - request_mock: mock.MagicMock, - bound_volume, - server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_volume.attach(server) - - request_mock.assert_called_with( - method="POST", - url="/volumes/14/actions/attach", - json={"server": 1}, - ) - assert action.id == 1 - assert action.progress == 0 - - @pytest.mark.parametrize( - "server", (Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))) - ) - def test_attach_with_automount( - self, - request_mock: mock.MagicMock, - bound_volume, - server, - action_response, - ): - request_mock.return_value = action_response - - action = bound_volume.attach(server, False) - - request_mock.assert_called_with( - method="POST", - url="/volumes/14/actions/attach", - json={"server": 1, "automount": False}, - ) - assert action.id == 1 - assert action.progress == 0 - - def test_detach( - self, - request_mock: mock.MagicMock, - bound_volume, - action_response, - ): - request_mock.return_value = action_response - - action = bound_volume.detach() - - request_mock.assert_called_with( - method="POST", - url="/volumes/14/actions/detach", - ) - assert action.id == 1 - assert action.progress == 0 - - def test_resize( - self, - request_mock: mock.MagicMock, - bound_volume, - action_response, - ): - request_mock.return_value = action_response - - action = bound_volume.resize(50) - - request_mock.assert_called_with( - method="POST", - url="/volumes/14/actions/resize", - json={"size": 50}, - ) - assert action.id == 1 - assert action.progress == 0 - class TestVolumesClient: @pytest.fixture() @@ -343,7 +224,11 @@ def test_create_with_server( request_mock.return_value = volume_create_response volumes_client.create( - 100, "database-storage", server=server, automount=False, format="xfs" + size=100, + name="database-storage", + server=server, + automount=False, + format="xfs", ) request_mock.assert_called_with( @@ -476,12 +361,12 @@ def test_attach( ): request_mock.return_value = action_response - action = volumes_client.attach(volume, server) + action = volumes_client.attach(volume, server, True) request_mock.assert_called_with( method="POST", url="/volumes/12/actions/attach", - json={"server": 1}, + json={"server": 1, "automount": True}, ) assert action.id == 1 assert action.progress == 0 diff --git a/tests/unit/zones/test_client.py b/tests/unit/zones/test_client.py index 91165ad1..e20aaedd 100644 --- a/tests/unit/zones/test_client.py +++ b/tests/unit/zones/test_client.py @@ -19,7 +19,7 @@ ZonesClient, ) -from ..conftest import assert_bound_action1 +from ..conftest import BoundModelTestCase, assert_bound_action1 def assert_bound_zone1(o: BoundZone, client: ZonesClient): @@ -847,13 +847,35 @@ def test_set_rrset_records( assert_bound_action1(action, resource_client._parent.actions) -class TestBoundZone: +class TestBoundZone(BoundModelTestCase): + methods = [ + BoundZone.update, + BoundZone.delete, + BoundZone.import_zonefile, + BoundZone.export_zonefile, + BoundZone.change_primary_nameservers, + BoundZone.change_ttl, + BoundZone.change_protection, + BoundZone.get_rrset_all, + BoundZone.get_rrset_list, + BoundZone.get_rrset, + BoundZone.create_rrset, + # With rrset sub resource + (BoundZone.update_rrset, {"sub_resource": True}), + (BoundZone.delete_rrset, {"sub_resource": True}), + (BoundZone.change_rrset_protection, {"sub_resource": True}), + (BoundZone.change_rrset_ttl, {"sub_resource": True}), + (BoundZone.add_rrset_records, {"sub_resource": True}), + (BoundZone.remove_rrset_records, {"sub_resource": True}), + (BoundZone.set_rrset_records, {"sub_resource": True}), + ] + @pytest.fixture() def resource_client(self, client: Client): return client.zones @pytest.fixture() - def bound_model(self, resource_client, zone1): + def bound_model(self, resource_client: ZonesClient, zone1): return BoundZone(resource_client, data=zone1) def test_init(self, resource_client: ZonesClient, bound_model: BoundZone): @@ -898,13 +920,23 @@ def test_init(self, resource_client: ZonesClient, bound_model: BoundZone): assert o.registrar == "hetzner" -class TestBoundZoneRRSet: +class TestBoundZoneRRSet(BoundModelTestCase): + methods = [ + BoundZoneRRSet.update_rrset, + BoundZoneRRSet.delete_rrset, + BoundZoneRRSet.change_rrset_protection, + BoundZoneRRSet.change_rrset_ttl, + BoundZoneRRSet.add_rrset_records, + BoundZoneRRSet.remove_rrset_records, + BoundZoneRRSet.set_rrset_records, + ] + @pytest.fixture() def resource_client(self, client: Client): return client.zones @pytest.fixture() - def bound_model(self, resource_client, zone_rrset1): + def bound_model(self, resource_client: ZonesClient, zone_rrset1): return BoundZoneRRSet(resource_client, data=zone_rrset1) def test_init(self, resource_client: ZonesClient, bound_model: BoundZoneRRSet):