import pytest import shutil from typing import Generator from os import mkdir from selfprivacy_api.utils.block_devices import BlockDevices import selfprivacy_api.services as service_module from selfprivacy_api.services import ServiceManager from selfprivacy_api.services.service import Service, ServiceStatus from selfprivacy_api.services.test_service import DummyService from tests.common import generate_service_query from tests.test_graphql.common import assert_empty, assert_ok, get_data from tests.test_graphql.test_system_nixos_tasks import prepare_nixos_rebuild_calls from tests.test_dkim import dkim_file LSBLK_BLOCKDEVICES_DICTS = [ { "name": "sda1", "path": "/dev/sda1", "fsavail": "4614107136", "fssize": "19814920192", "fstype": "ext4", "fsused": "14345314304", "mountpoints": ["/nix/store", "/"], "label": None, "uuid": "ec80c004-baec-4a2c-851d-0e1807135511", "size": 20210236928, "model": None, "serial": None, "type": "part", }, { "name": "sda2", "path": "/dev/sda2", "fsavail": "4614107136", "fssize": "19814920192", "fstype": "ext4", "fsused": "14345314304", "mountpoints": ["/home"], "label": None, "uuid": "deadbeef-baec-4a2c-851d-0e1807135511", "size": 20210236928, "model": None, "serial": None, "type": "part", }, ] @pytest.fixture() def mock_lsblk_devices(mocker): mock = mocker.patch( "selfprivacy_api.utils.block_devices.BlockDevices.lsblk_device_dicts", autospec=True, return_value=LSBLK_BLOCKDEVICES_DICTS, ) BlockDevices().update() assert BlockDevices().lsblk_device_dicts() == LSBLK_BLOCKDEVICES_DICTS devices = BlockDevices().get_block_devices() assert len(devices) == 2 names = [device.name for device in devices] assert "sda1" in names assert "sda2" in names return mock @pytest.fixture() def dummy_service_with_binds(dummy_service, mock_lsblk_devices, volume_folders): binds = dummy_service.binds() for bind in binds: path = bind.binding_path shutil.move(bind.binding_path, bind.location_at_volume()) mkdir(bind.binding_path) bind.ensure_ownership() bind.validate() bind.bind() return dummy_service @pytest.fixture() def only_dummy_service(dummy_service) -> Generator[DummyService, None, None]: # because queries to services that are not really there error out back_copy = service_module.services.copy() service_module.services.clear() service_module.services.append(dummy_service) yield dummy_service service_module.services.clear() service_module.services.extend(back_copy) @pytest.fixture def only_dummy_service_and_api( only_dummy_service, generic_userdata, dkim_file ) -> Generator[DummyService, None, None]: service_module.services.append(ServiceManager()) return only_dummy_service @pytest.fixture() def mock_check_volume(mocker): mock = mocker.patch( "selfprivacy_api.services.service.check_volume", autospec=True, return_value=None, ) return mock API_START_MUTATION = """ mutation TestStartService($service_id: String!) { services { startService(serviceId: $service_id) { success message code service { id status } } } } """ API_RESTART_MUTATION = """ mutation TestRestartService($service_id: String!) { services { restartService(serviceId: $service_id) { success message code service { id status } } } } """ API_ENABLE_MUTATION = """ mutation TestStartService($service_id: String!) { services { enableService(serviceId: $service_id) { success message code service { id isEnabled } } } } """ API_DISABLE_MUTATION = """ mutation TestStartService($service_id: String!) { services { disableService(serviceId: $service_id) { success message code service { id isEnabled } } } } """ API_STOP_MUTATION = """ mutation TestStopService($service_id: String!) { services { stopService(serviceId: $service_id) { success message code service { id status } } } } """ API_SERVICES_QUERY = """ allServices { id status isEnabled url } """ API_MOVE_MUTATION = """ mutation TestMoveService($input: MoveServiceInput!) { services { moveService(input: $input) { success message code job { uid status } service { id status } } } } """ def assert_notfound(data): assert_errorcode(data, 404) def assert_errorcode(data, errorcode): assert data["code"] == errorcode assert data["success"] is False assert data["message"] is not None def api_enable(client, service: Service) -> dict: return api_enable_by_name(client, service.get_id()) def api_enable_by_name(client, service_id: str) -> dict: response = client.post( "/graphql", json={ "query": API_ENABLE_MUTATION, "variables": {"service_id": service_id}, }, ) return response def api_disable(client, service: Service) -> dict: return api_disable_by_name(client, service.get_id()) def api_disable_by_name(client, service_id: str) -> dict: response = client.post( "/graphql", json={ "query": API_DISABLE_MUTATION, "variables": {"service_id": service_id}, }, ) return response def api_start(client, service: Service) -> dict: return api_start_by_name(client, service.get_id()) def api_start_by_name(client, service_id: str) -> dict: response = client.post( "/graphql", json={ "query": API_START_MUTATION, "variables": {"service_id": service_id}, }, ) return response def api_move(client, service: Service, location: str) -> dict: return api_move_by_name(client, service.get_id(), location) def api_move_by_name(client, service_id: str, location: str) -> dict: response = client.post( "/graphql", json={ "query": API_MOVE_MUTATION, "variables": { "input": { "serviceId": service_id, "location": location, } }, }, ) return response def api_restart(client, service: Service) -> dict: return api_restart_by_name(client, service.get_id()) def api_restart_by_name(client, service_id: str) -> dict: response = client.post( "/graphql", json={ "query": API_RESTART_MUTATION, "variables": {"service_id": service_id}, }, ) return response def api_stop(client, service: Service) -> dict: return api_stop_by_name(client, service.get_id()) def api_stop_by_name(client, service_id: str) -> dict: response = client.post( "/graphql", json={ "query": API_STOP_MUTATION, "variables": {"service_id": service_id}, }, ) return response def api_all_services(authorized_client): response = api_all_services_raw(authorized_client) data = get_data(response) result = data["services"]["allServices"] assert result is not None return result def api_all_services_raw(client): return client.post( "/graphql", json={"query": generate_service_query([API_SERVICES_QUERY])}, ) def api_service(authorized_client, service: Service): id = service.get_id() for _service in api_all_services(authorized_client): if _service["id"] == id: return _service def test_get_services(authorized_client, only_dummy_service): services = api_all_services(authorized_client) assert len(services) == 1 api_dummy_service = services[0] assert api_dummy_service["id"] == "testservice" assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value assert api_dummy_service["isEnabled"] is True assert api_dummy_service["url"] == "https://test.test-domain.tld" def test_enable_return_value(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_enable(authorized_client, dummy_service) data = get_data(mutation_response)["services"]["enableService"] assert_ok(data) service = data["service"] assert service["id"] == dummy_service.get_id() assert service["isEnabled"] == True def test_disable_return_value(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_disable(authorized_client, dummy_service) data = get_data(mutation_response)["services"]["disableService"] assert_ok(data) service = data["service"] assert service["id"] == dummy_service.get_id() assert service["isEnabled"] == False def test_start_return_value(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_start(authorized_client, dummy_service) data = get_data(mutation_response)["services"]["startService"] assert_ok(data) service = data["service"] assert service["id"] == dummy_service.get_id() assert service["status"] == ServiceStatus.ACTIVE.value def test_restart(authorized_client, only_dummy_service): dummy_service = only_dummy_service dummy_service.set_delay(0.3) mutation_response = api_restart(authorized_client, dummy_service) data = get_data(mutation_response)["services"]["restartService"] assert_ok(data) service = data["service"] assert service["id"] == dummy_service.get_id() assert service["status"] == ServiceStatus.RELOADING.value def test_stop_return_value(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_stop(authorized_client, dummy_service) data = get_data(mutation_response)["services"]["stopService"] assert_ok(data) service = data["service"] assert service["id"] == dummy_service.get_id() assert service["status"] == ServiceStatus.INACTIVE.value def test_allservices_unauthorized(client, only_dummy_service): dummy_service = only_dummy_service response = api_all_services_raw(client) assert response.status_code == 200 assert response.json().get("data") is None def test_start_unauthorized(client, only_dummy_service): dummy_service = only_dummy_service response = api_start(client, dummy_service) assert_empty(response) def test_restart_unauthorized(client, only_dummy_service): dummy_service = only_dummy_service response = api_restart(client, dummy_service) assert_empty(response) def test_stop_unauthorized(client, only_dummy_service): dummy_service = only_dummy_service response = api_stop(client, dummy_service) assert_empty(response) def test_enable_unauthorized(client, only_dummy_service): dummy_service = only_dummy_service response = api_enable(client, dummy_service) assert_empty(response) def test_disable_unauthorized(client, only_dummy_service): dummy_service = only_dummy_service response = api_disable(client, dummy_service) assert_empty(response) def test_move_unauthorized(client, only_dummy_service): dummy_service = only_dummy_service response = api_move(client, dummy_service, "sda1") assert_empty(response) def test_start_nonexistent(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_start_by_name(authorized_client, "bogus_service") data = get_data(mutation_response)["services"]["startService"] assert_notfound(data) assert data["service"] is None def test_restart_nonexistent(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_restart_by_name(authorized_client, "bogus_service") data = get_data(mutation_response)["services"]["restartService"] assert_notfound(data) assert data["service"] is None def test_stop_nonexistent(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_stop_by_name(authorized_client, "bogus_service") data = get_data(mutation_response)["services"]["stopService"] assert_notfound(data) assert data["service"] is None def test_enable_nonexistent(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_enable_by_name(authorized_client, "bogus_service") data = get_data(mutation_response)["services"]["enableService"] assert_notfound(data) assert data["service"] is None def test_disable_nonexistent(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_disable_by_name(authorized_client, "bogus_service") data = get_data(mutation_response)["services"]["disableService"] assert_notfound(data) assert data["service"] is None def test_stop_start(authorized_client, only_dummy_service): dummy_service = only_dummy_service api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value # attempting to start an already started service api_start(authorized_client, dummy_service) api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value api_stop(authorized_client, dummy_service) api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["status"] == ServiceStatus.INACTIVE.value # attempting to stop an already stopped service api_stop(authorized_client, dummy_service) api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["status"] == ServiceStatus.INACTIVE.value api_start(authorized_client, dummy_service) api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value def test_disable_enable(authorized_client, only_dummy_service): dummy_service = only_dummy_service api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["isEnabled"] is True # attempting to enable an already enableed service api_enable(authorized_client, dummy_service) api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["isEnabled"] is True assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value api_disable(authorized_client, dummy_service) api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["isEnabled"] is False assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value # attempting to disable an already disableped service api_disable(authorized_client, dummy_service) api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["isEnabled"] is False assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value api_enable(authorized_client, dummy_service) api_dummy_service = api_all_services(authorized_client)[0] assert api_dummy_service["isEnabled"] is True assert api_dummy_service["status"] == ServiceStatus.ACTIVE.value def test_move_immovable(authorized_client, dummy_service_with_binds): dummy_service = dummy_service_with_binds dummy_service.set_movable(False) root = BlockDevices().get_root_block_device() mutation_response = api_move(authorized_client, dummy_service, root.name) data = get_data(mutation_response)["services"]["moveService"] assert_errorcode(data, 400) try: assert "not movable" in data["message"] except AssertionError: raise ValueError("wrong type of error?: ", data["message"]) # is there a meaning in returning the service in this? assert data["service"] is not None assert data["job"] is None def test_move_no_such_service(authorized_client, only_dummy_service): mutation_response = api_move_by_name(authorized_client, "bogus_service", "sda1") data = get_data(mutation_response)["services"]["moveService"] assert_errorcode(data, 404) assert data["service"] is None assert data["job"] is None def test_move_no_such_volume(authorized_client, only_dummy_service): dummy_service = only_dummy_service mutation_response = api_move(authorized_client, dummy_service, "bogus_volume") data = get_data(mutation_response)["services"]["moveService"] assert_notfound(data) assert data["service"] is None assert data["job"] is None def test_move_same_volume(authorized_client, dummy_service): # dummy_service = only_dummy_service # we need a drive that actually exists root_volume = BlockDevices().get_root_block_device() dummy_service.set_simulated_moves(False) dummy_service.set_drive(root_volume.name) mutation_response = api_move(authorized_client, dummy_service, root_volume.name) data = get_data(mutation_response)["services"]["moveService"] assert_errorcode(data, 400) # is there a meaning in returning the service in this? assert data["service"] is not None # We do not create a job if task is not created assert data["job"] is None def test_graphql_move_service_without_folders_on_old_volume( authorized_client, generic_userdata, mock_lsblk_devices, dummy_service: DummyService, ): """ Situation when you have folders in the filetree but they are not mounted but just folders """ target = "sda1" BlockDevices().update() assert BlockDevices().get_block_device(target) is not None dummy_service.set_simulated_moves(False) dummy_service.set_drive("sda2") mutation_response = api_move(authorized_client, dummy_service, target) data = get_data(mutation_response)["services"]["moveService"] assert_errorcode(data, 400) assert "sda2/test_service is not found" in data["message"] def test_move_empty( authorized_client, generic_userdata, mock_check_volume, dummy_service, fp ): """ A reregister of uninitialized service with no data. No binds in place yet, and no rebuilds should happen. """ origin = "sda1" target = "sda2" assert BlockDevices().get_block_device(target) is not None assert BlockDevices().get_block_device(origin) is not None dummy_service.set_drive(origin) dummy_service.set_simulated_moves(False) dummy_service.disable() unit_name = "sp-nixos-rebuild.service" rebuild_command = ["systemctl", "start", unit_name] prepare_nixos_rebuild_calls(fp, unit_name) # We will NOT be mounting and remounting folders mount_command = ["mount", fp.any()] unmount_command = ["umount", fp.any()] fp.pass_command(mount_command, 2) fp.pass_command(unmount_command, 2) # We will NOT be changing ownership chown_command = ["chown", fp.any()] fp.pass_command(chown_command, 2) # We have virtual binds encapsulating our understanding where this should go. assert len(dummy_service.binds()) == 2 # Remove all folders for folder in dummy_service.get_folders(): shutil.rmtree(folder) # They are virtual and unaffected by folder removal assert len(dummy_service.binds()) == 2 mutation_response = api_move(authorized_client, dummy_service, target) data = get_data(mutation_response)["services"]["moveService"] assert_ok(data) assert data["service"] is not None assert fp.call_count(rebuild_command) == 0 assert fp.call_count(mount_command) == 0 assert fp.call_count(unmount_command) == 0 assert fp.call_count(chown_command) == 0 def test_graphql_move_service( authorized_client, generic_userdata, mock_check_volume, dummy_service_with_binds, fp ): dummy_service = dummy_service_with_binds origin = "sda1" target = "sda2" assert BlockDevices().get_block_device(target) is not None assert BlockDevices().get_block_device(origin) is not None dummy_service.set_drive(origin) dummy_service.set_simulated_moves(False) unit_name = "sp-nixos-rebuild.service" rebuild_command = ["systemctl", "start", unit_name] prepare_nixos_rebuild_calls(fp, unit_name) # We will be mounting and remounting folders mount_command = ["mount", fp.any()] unmount_command = ["umount", fp.any()] fp.pass_command(mount_command, 2) fp.pass_command(unmount_command, 2) # We will be changing ownership chown_command = ["chown", fp.any()] fp.pass_command(chown_command, 2) mutation_response = api_move(authorized_client, dummy_service, target) data = get_data(mutation_response)["services"]["moveService"] assert_ok(data) assert data["service"] is not None assert fp.call_count(rebuild_command) == 1 assert fp.call_count(mount_command) == 2 assert fp.call_count(unmount_command) == 2 assert fp.call_count(chown_command) == 2 def test_mailservice_cannot_enable_disable(authorized_client): mailservice = ServiceManager.get_service_by_id("simple-nixos-mailserver") mutation_response = api_enable(authorized_client, mailservice) data = get_data(mutation_response)["services"]["enableService"] assert_errorcode(data, 400) # TODO?: we cannot convert mailservice to graphql Service without /var/domain yet # assert data["service"] is not None mutation_response = api_disable(authorized_client, mailservice) data = get_data(mutation_response)["services"]["disableService"] assert_errorcode(data, 400) # assert data["service"] is not None