diff --git a/selfprivacy_api/actions/system.py b/selfprivacy_api/actions/system.py index 853662f..f5e0dc0 100644 --- a/selfprivacy_api/actions/system.py +++ b/selfprivacy_api/actions/system.py @@ -2,7 +2,7 @@ import os import subprocess import pytz -from typing import Optional +from typing import Optional, List from pydantic import BaseModel from selfprivacy_api.utils import WriteUserData, ReadUserData @@ -58,36 +58,56 @@ def set_auto_upgrade_settings( user_data["autoUpgrade"]["allowReboot"] = allowReboot +class ShellException(Exception): + """Something went wrong when calling another process""" + + pass + + +def run_blocking(cmd: List[str], new_session: bool = False) -> str: + """Run a process, block until done, return output, complain if failed""" + process_handle = subprocess.Popen( + cmd, + shell=False, + start_new_session=new_session, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout_raw, stderr_raw = process_handle.communicate() + stdout = stdout_raw.decode("utf-8") + if stderr_raw is not None: + stderr = stderr_raw.decode("utf-8") + else: + stderr = "" + output = stdout + "\n" + stderr + if process_handle.returncode != 0: + raise ShellException( + f"Shell command failed, command array: {cmd}, output: {output}" + ) + return stdout + + def rebuild_system() -> int: """Rebuild the system""" - rebuild_result = subprocess.Popen( - ["systemctl", "start", "sp-nixos-rebuild.service"], start_new_session=True - ) - rebuild_result.communicate()[0] - return rebuild_result.returncode + run_blocking(["systemctl", "start", "sp-nixos-rebuild.service"], new_session=True) + return 0 def rollback_system() -> int: """Rollback the system""" - rollback_result = subprocess.Popen( - ["systemctl", "start", "sp-nixos-rollback.service"], start_new_session=True - ) - rollback_result.communicate()[0] - return rollback_result.returncode + run_blocking(["systemctl", "start", "sp-nixos-rollback.service"], new_session=True) + return 0 def upgrade_system() -> int: """Upgrade the system""" - upgrade_result = subprocess.Popen( - ["systemctl", "start", "sp-nixos-upgrade.service"], start_new_session=True - ) - upgrade_result.communicate()[0] - return upgrade_result.returncode + run_blocking(["systemctl", "start", "sp-nixos-upgrade.service"], new_session=True) + return 0 def reboot_system() -> None: """Reboot the system""" - subprocess.Popen(["reboot"], start_new_session=True) + run_blocking(["reboot"], new_session=True) def get_system_version() -> str: diff --git a/selfprivacy_api/actions/users.py b/selfprivacy_api/actions/users.py index bfc1756..fafa84f 100644 --- a/selfprivacy_api/actions/users.py +++ b/selfprivacy_api/actions/users.py @@ -58,7 +58,7 @@ def get_users( ) for user in user_data["users"] ] - if not exclude_primary: + if not exclude_primary and "username" in user_data.keys(): users.append( UserDataUser( username=user_data["username"], @@ -107,6 +107,12 @@ class PasswordIsEmpty(Exception): pass +class InvalidConfiguration(Exception): + """The userdata is broken""" + + pass + + def create_user(username: str, password: str): if password == "": raise PasswordIsEmpty("Password is empty") @@ -124,6 +130,10 @@ def create_user(username: str, password: str): with ReadUserData() as user_data: ensure_ssh_and_users_fields_exist(user_data) + if "username" not in user_data.keys(): + raise InvalidConfiguration( + "Broken config: Admin name is not defined. Consider recovery or add it manually" + ) if username == user_data["username"]: raise UserAlreadyExists("User already exists") if username in [user["username"] for user in user_data["users"]]: diff --git a/selfprivacy_api/app.py b/selfprivacy_api/app.py index a58301a..64ca85a 100644 --- a/selfprivacy_api/app.py +++ b/selfprivacy_api/app.py @@ -10,12 +10,6 @@ from selfprivacy_api.dependencies import get_api_version from selfprivacy_api.graphql.schema import schema from selfprivacy_api.migrations import run_migrations -from selfprivacy_api.rest import ( - system, - users, - api_auth, - services, -) app = FastAPI() @@ -32,10 +26,6 @@ app.add_middleware( ) -app.include_router(system.router) -app.include_router(users.router) -app.include_router(api_auth.router) -app.include_router(services.router) app.include_router(graphql_app, prefix="/graphql") diff --git a/selfprivacy_api/backup/backuppers/restic_backupper.py b/selfprivacy_api/backup/backuppers/restic_backupper.py index fd653e6..0d74d9c 100644 --- a/selfprivacy_api/backup/backuppers/restic_backupper.py +++ b/selfprivacy_api/backup/backuppers/restic_backupper.py @@ -197,6 +197,8 @@ class ResticBackupper(AbstractBackupper): output, "parsed messages:", messages, + "command: ", + backup_command, ) from error @staticmethod diff --git a/selfprivacy_api/backup/tasks.py b/selfprivacy_api/backup/tasks.py index f0422ca..6520c70 100644 --- a/selfprivacy_api/backup/tasks.py +++ b/selfprivacy_api/backup/tasks.py @@ -11,7 +11,9 @@ from selfprivacy_api.graphql.common_types.backup import ( from selfprivacy_api.models.backup.snapshot import Snapshot from selfprivacy_api.utils.huey import huey from huey import crontab + from selfprivacy_api.services.service import Service +from selfprivacy_api.services import get_service_by_id from selfprivacy_api.backup import Backups from selfprivacy_api.jobs import Jobs, JobStatus, Job @@ -31,12 +33,13 @@ def validate_datetime(dt: datetime) -> bool: # huey tasks need to return something @huey.task() -def start_backup( - service: Service, reason: BackupReason = BackupReason.EXPLICIT -) -> bool: +def start_backup(service_id: str, reason: BackupReason = BackupReason.EXPLICIT) -> bool: """ The worker task that starts the backup process. """ + service = get_service_by_id(service_id) + if service is None: + raise ValueError(f"No such service: {service_id}") Backups.back_up(service, reason) return True diff --git a/selfprivacy_api/dependencies.py b/selfprivacy_api/dependencies.py index 05c9bdc..1dfc0a9 100644 --- a/selfprivacy_api/dependencies.py +++ b/selfprivacy_api/dependencies.py @@ -27,4 +27,4 @@ async def get_token_header( def get_api_version() -> str: """Get API version""" - return "2.4.3" + return "3.0.0" diff --git a/selfprivacy_api/graphql/mutations/backup_mutations.py b/selfprivacy_api/graphql/mutations/backup_mutations.py index cc1538e..820564c 100644 --- a/selfprivacy_api/graphql/mutations/backup_mutations.py +++ b/selfprivacy_api/graphql/mutations/backup_mutations.py @@ -148,7 +148,7 @@ class BackupMutations: ) job = add_backup_job(service) - start_backup(service) + start_backup(service_id) return GenericJobMutationReturn( success=True, diff --git a/selfprivacy_api/graphql/mutations/services_mutations.py b/selfprivacy_api/graphql/mutations/services_mutations.py index 86cab10..9bacf66 100644 --- a/selfprivacy_api/graphql/mutations/services_mutations.py +++ b/selfprivacy_api/graphql/mutations/services_mutations.py @@ -4,6 +4,7 @@ import typing import strawberry from selfprivacy_api.graphql import IsAuthenticated from selfprivacy_api.graphql.common_types.jobs import job_to_api_job +from selfprivacy_api.jobs import JobStatus from selfprivacy_api.graphql.common_types.service import ( Service, @@ -47,14 +48,22 @@ class ServicesMutations: @strawberry.mutation(permission_classes=[IsAuthenticated]) def enable_service(self, service_id: str) -> ServiceMutationReturn: """Enable service.""" - service = get_service_by_id(service_id) - if service is None: + try: + service = get_service_by_id(service_id) + if service is None: + return ServiceMutationReturn( + success=False, + message="Service not found.", + code=404, + ) + service.enable() + except Exception as e: return ServiceMutationReturn( success=False, - message="Service not found.", - code=404, + message=format_error(e), + code=400, ) - service.enable() + return ServiceMutationReturn( success=True, message="Service enabled.", @@ -65,14 +74,21 @@ class ServicesMutations: @strawberry.mutation(permission_classes=[IsAuthenticated]) def disable_service(self, service_id: str) -> ServiceMutationReturn: """Disable service.""" - service = get_service_by_id(service_id) - if service is None: + try: + service = get_service_by_id(service_id) + if service is None: + return ServiceMutationReturn( + success=False, + message="Service not found.", + code=404, + ) + service.disable() + except Exception as e: return ServiceMutationReturn( success=False, - message="Service not found.", - code=404, + message=format_error(e), + code=400, ) - service.disable() return ServiceMutationReturn( success=True, message="Service disabled.", @@ -144,6 +160,8 @@ class ServicesMutations: message="Service not found.", code=404, ) + # TODO: make serviceImmovable and BlockdeviceNotFound exceptions + # in the move_to_volume() function and handle them here if not service.is_movable(): return ServiceJobMutationReturn( success=False, @@ -160,10 +178,31 @@ class ServicesMutations: service=service_to_graphql_service(service), ) job = service.move_to_volume(volume) - return ServiceJobMutationReturn( - success=True, - message="Service moved.", - code=200, - service=service_to_graphql_service(service), - job=job_to_api_job(job), - ) + if job.status in [JobStatus.CREATED, JobStatus.RUNNING]: + return ServiceJobMutationReturn( + success=True, + message="Started moving the service.", + code=200, + service=service_to_graphql_service(service), + job=job_to_api_job(job), + ) + elif job.status == JobStatus.FINISHED: + return ServiceJobMutationReturn( + success=True, + message="Service moved.", + code=200, + service=service_to_graphql_service(service), + job=job_to_api_job(job), + ) + else: + return ServiceJobMutationReturn( + success=False, + message=f"Service move failure: {job.status_text}", + code=400, + service=service_to_graphql_service(service), + job=job_to_api_job(job), + ) + + +def format_error(e: Exception) -> str: + return type(e).__name__ + ": " + str(e) diff --git a/selfprivacy_api/graphql/mutations/system_mutations.py b/selfprivacy_api/graphql/mutations/system_mutations.py index daada17..13ac16b 100644 --- a/selfprivacy_api/graphql/mutations/system_mutations.py +++ b/selfprivacy_api/graphql/mutations/system_mutations.py @@ -9,6 +9,7 @@ from selfprivacy_api.graphql.mutations.mutation_interface import ( ) import selfprivacy_api.actions.system as system_actions +import selfprivacy_api.actions.ssh as ssh_actions @strawberry.type @@ -26,6 +27,22 @@ class AutoUpgradeSettingsMutationReturn(MutationReturnInterface): allowReboot: bool +@strawberry.type +class SSHSettingsMutationReturn(MutationReturnInterface): + """A return type for after changing SSH settings""" + + enable: bool + password_authentication: bool + + +@strawberry.input +class SSHSettingsInput: + """Input type for SSH settings""" + + enable: bool + password_authentication: bool + + @strawberry.input class AutoUpgradeSettingsInput: """Input type for auto upgrade settings""" @@ -77,40 +94,88 @@ class SystemMutations: ) @strawberry.mutation(permission_classes=[IsAuthenticated]) - def run_system_rebuild(self) -> GenericMutationReturn: - system_actions.rebuild_system() - return GenericMutationReturn( - success=True, - message="Starting rebuild system", - code=200, + def change_ssh_settings( + self, settings: SSHSettingsInput + ) -> SSHSettingsMutationReturn: + """Change ssh settings of the server.""" + ssh_actions.set_ssh_settings( + enable=settings.enable, + password_authentication=settings.password_authentication, ) + new_settings = ssh_actions.get_ssh_settings() + + return SSHSettingsMutationReturn( + success=True, + message="SSH settings changed", + code=200, + enable=new_settings.enable, + password_authentication=new_settings.passwordAuthentication, + ) + + @strawberry.mutation(permission_classes=[IsAuthenticated]) + def run_system_rebuild(self) -> GenericMutationReturn: + try: + system_actions.rebuild_system() + return GenericMutationReturn( + success=True, + message="Starting rebuild system", + code=200, + ) + except system_actions.ShellException as e: + return GenericMutationReturn( + success=False, + message=str(e), + code=500, + ) + @strawberry.mutation(permission_classes=[IsAuthenticated]) def run_system_rollback(self) -> GenericMutationReturn: system_actions.rollback_system() - return GenericMutationReturn( - success=True, - message="Starting rebuild system", - code=200, - ) + try: + return GenericMutationReturn( + success=True, + message="Starting rebuild system", + code=200, + ) + except system_actions.ShellException as e: + return GenericMutationReturn( + success=False, + message=str(e), + code=500, + ) @strawberry.mutation(permission_classes=[IsAuthenticated]) def run_system_upgrade(self) -> GenericMutationReturn: system_actions.upgrade_system() - return GenericMutationReturn( - success=True, - message="Starting rebuild system", - code=200, - ) + try: + return GenericMutationReturn( + success=True, + message="Starting rebuild system", + code=200, + ) + except system_actions.ShellException as e: + return GenericMutationReturn( + success=False, + message=str(e), + code=500, + ) @strawberry.mutation(permission_classes=[IsAuthenticated]) def reboot_system(self) -> GenericMutationReturn: system_actions.reboot_system() - return GenericMutationReturn( - success=True, - message="System reboot has started", - code=200, - ) + try: + return GenericMutationReturn( + success=True, + message="System reboot has started", + code=200, + ) + except system_actions.ShellException as e: + return GenericMutationReturn( + success=False, + message=str(e), + code=500, + ) @strawberry.mutation(permission_classes=[IsAuthenticated]) def pull_repository_changes(self) -> GenericMutationReturn: diff --git a/selfprivacy_api/graphql/mutations/users_mutations.py b/selfprivacy_api/graphql/mutations/users_mutations.py index 57825bc..7644b90 100644 --- a/selfprivacy_api/graphql/mutations/users_mutations.py +++ b/selfprivacy_api/graphql/mutations/users_mutations.py @@ -69,6 +69,12 @@ class UsersMutations: message=str(e), code=400, ) + except users_actions.InvalidConfiguration as e: + return UserMutationReturn( + success=False, + message=str(e), + code=400, + ) except users_actions.UserAlreadyExists as e: return UserMutationReturn( success=False, diff --git a/selfprivacy_api/migrations/__init__.py b/selfprivacy_api/migrations/__init__.py index 4aa932c..f2d1f0d 100644 --- a/selfprivacy_api/migrations/__init__.py +++ b/selfprivacy_api/migrations/__init__.py @@ -19,6 +19,7 @@ from selfprivacy_api.migrations.migrate_to_selfprivacy_channel import ( ) from selfprivacy_api.migrations.mount_volume import MountVolume from selfprivacy_api.migrations.providers import CreateProviderFields +from selfprivacy_api.migrations.modules_in_json import CreateModulesField from selfprivacy_api.migrations.prepare_for_nixos_2211 import ( MigrateToSelfprivacyChannelFrom2205, ) @@ -37,6 +38,7 @@ migrations = [ MigrateToSelfprivacyChannelFrom2205(), MigrateToSelfprivacyChannelFrom2211(), LoadTokensToRedis(), + CreateModulesField(), ] diff --git a/selfprivacy_api/migrations/modules_in_json.py b/selfprivacy_api/migrations/modules_in_json.py new file mode 100644 index 0000000..64ba7d3 --- /dev/null +++ b/selfprivacy_api/migrations/modules_in_json.py @@ -0,0 +1,50 @@ +from selfprivacy_api.migrations.migration import Migration +from selfprivacy_api.utils import ReadUserData, WriteUserData +from selfprivacy_api.services import get_all_services + + +def migrate_services_to_modules(): + with WriteUserData() as userdata: + if "modules" not in userdata.keys(): + userdata["modules"] = {} + + for service in get_all_services(): + name = service.get_id() + if name in userdata.keys(): + field_content = userdata[name] + userdata["modules"][name] = field_content + del userdata[name] + + +# If you ever want to get rid of modules field you will need to get rid of this migration +class CreateModulesField(Migration): + """introduce 'modules' (services) into userdata""" + + def get_migration_name(self): + return "modules_in_json" + + def get_migration_description(self): + return "Group service settings into a 'modules' field in userdata.json" + + def is_migration_needed(self) -> bool: + try: + with ReadUserData() as userdata: + for service in get_all_services(): + if service.get_id() in userdata.keys(): + return True + + if "modules" not in userdata.keys(): + return True + return False + except Exception as e: + print(e) + return False + + def migrate(self): + # Write info about providers to userdata.json + try: + migrate_services_to_modules() + print("Done") + except Exception as e: + print(e) + print("Error migrating service fields") diff --git a/selfprivacy_api/rest/__init__.py b/selfprivacy_api/rest/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/selfprivacy_api/rest/api_auth.py b/selfprivacy_api/rest/api_auth.py deleted file mode 100644 index 275dac3..0000000 --- a/selfprivacy_api/rest/api_auth.py +++ /dev/null @@ -1,125 +0,0 @@ -from datetime import datetime -from typing import Optional -from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel -from selfprivacy_api.actions.api_tokens import ( - CannotDeleteCallerException, - InvalidExpirationDate, - InvalidUsesLeft, - NotFoundException, - delete_api_token, - refresh_api_token, - get_api_recovery_token_status, - get_api_tokens_with_caller_flag, - get_new_api_recovery_key, - use_mnemonic_recovery_token, - delete_new_device_auth_token, - get_new_device_auth_token, - use_new_device_auth_token, -) - -from selfprivacy_api.dependencies import TokenHeader, get_token_header - - -router = APIRouter( - prefix="/auth", - tags=["auth"], - responses={404: {"description": "Not found"}}, -) - - -@router.get("/tokens") -async def rest_get_tokens(auth_token: TokenHeader = Depends(get_token_header)): - """Get the tokens info""" - return get_api_tokens_with_caller_flag(auth_token.token) - - -class DeleteTokenInput(BaseModel): - """Delete token input""" - - token_name: str - - -@router.delete("/tokens") -async def rest_delete_tokens( - token: DeleteTokenInput, auth_token: TokenHeader = Depends(get_token_header) -): - """Delete the tokens""" - try: - delete_api_token(auth_token.token, token.token_name) - except NotFoundException: - raise HTTPException(status_code=404, detail="Token not found") - except CannotDeleteCallerException: - raise HTTPException(status_code=400, detail="Cannot delete caller's token") - return {"message": "Token deleted"} - - -@router.post("/tokens") -async def rest_refresh_token(auth_token: TokenHeader = Depends(get_token_header)): - """Refresh the token""" - try: - new_token = refresh_api_token(auth_token.token) - except NotFoundException: - raise HTTPException(status_code=404, detail="Token not found") - return {"token": new_token} - - -@router.get("/recovery_token") -async def rest_get_recovery_token_status( - auth_token: TokenHeader = Depends(get_token_header), -): - return get_api_recovery_token_status() - - -class CreateRecoveryTokenInput(BaseModel): - expiration: Optional[datetime] = None - uses: Optional[int] = None - - -@router.post("/recovery_token") -async def rest_create_recovery_token( - limits: CreateRecoveryTokenInput = CreateRecoveryTokenInput(), - auth_token: TokenHeader = Depends(get_token_header), -): - try: - token = get_new_api_recovery_key(limits.expiration, limits.uses) - except InvalidExpirationDate as e: - raise HTTPException(status_code=400, detail=str(e)) - except InvalidUsesLeft as e: - raise HTTPException(status_code=400, detail=str(e)) - return {"token": token} - - -class UseTokenInput(BaseModel): - token: str - device: str - - -@router.post("/recovery_token/use") -async def rest_use_recovery_token(input: UseTokenInput): - token = use_mnemonic_recovery_token(input.token, input.device) - if token is None: - raise HTTPException(status_code=404, detail="Token not found") - return {"token": token} - - -@router.post("/new_device") -async def rest_new_device(auth_token: TokenHeader = Depends(get_token_header)): - token = get_new_device_auth_token() - return {"token": token} - - -@router.delete("/new_device") -async def rest_delete_new_device_token( - auth_token: TokenHeader = Depends(get_token_header), -): - delete_new_device_auth_token() - return {"token": None} - - -@router.post("/new_device/authorize") -async def rest_new_device_authorize(input: UseTokenInput): - token = use_new_device_auth_token(input.token, input.device) - if token is None: - raise HTTPException(status_code=404, detail="Token not found") - return {"message": "Device authorized", "token": token} diff --git a/selfprivacy_api/rest/services.py b/selfprivacy_api/rest/services.py deleted file mode 100644 index c6dc12e..0000000 --- a/selfprivacy_api/rest/services.py +++ /dev/null @@ -1,336 +0,0 @@ -"""Basic services legacy api""" -import base64 -from typing import Optional -from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel -from selfprivacy_api.actions.ssh import ( - InvalidPublicKey, - KeyAlreadyExists, - KeyNotFound, - create_ssh_key, - enable_ssh, - get_ssh_settings, - remove_ssh_key, - set_ssh_settings, -) -from selfprivacy_api.actions.users import UserNotFound, get_user_by_username - -from selfprivacy_api.dependencies import get_token_header -from selfprivacy_api.services.bitwarden import Bitwarden -from selfprivacy_api.services.gitea import Gitea -from selfprivacy_api.services.mailserver import MailServer -from selfprivacy_api.services.nextcloud import Nextcloud -from selfprivacy_api.services.ocserv import Ocserv -from selfprivacy_api.services.pleroma import Pleroma -from selfprivacy_api.services.service import ServiceStatus -from selfprivacy_api.utils import get_dkim_key, get_domain - -router = APIRouter( - prefix="/services", - tags=["services"], - dependencies=[Depends(get_token_header)], - responses={404: {"description": "Not found"}}, -) - - -def service_status_to_return_code(status: ServiceStatus): - """Converts service status object to return code for - compatibility with legacy api""" - if status == ServiceStatus.ACTIVE: - return 0 - elif status == ServiceStatus.FAILED: - return 1 - elif status == ServiceStatus.INACTIVE: - return 3 - elif status == ServiceStatus.OFF: - return 4 - else: - return 2 - - -@router.get("/status") -async def get_status(): - """Get the status of the services""" - mail_status = MailServer.get_status() - bitwarden_status = Bitwarden.get_status() - gitea_status = Gitea.get_status() - nextcloud_status = Nextcloud.get_status() - ocserv_stauts = Ocserv.get_status() - pleroma_status = Pleroma.get_status() - - return { - "imap": service_status_to_return_code(mail_status), - "smtp": service_status_to_return_code(mail_status), - "http": 0, - "bitwarden": service_status_to_return_code(bitwarden_status), - "gitea": service_status_to_return_code(gitea_status), - "nextcloud": service_status_to_return_code(nextcloud_status), - "ocserv": service_status_to_return_code(ocserv_stauts), - "pleroma": service_status_to_return_code(pleroma_status), - } - - -@router.post("/bitwarden/enable") -async def enable_bitwarden(): - """Enable Bitwarden""" - Bitwarden.enable() - return { - "status": 0, - "message": "Bitwarden enabled", - } - - -@router.post("/bitwarden/disable") -async def disable_bitwarden(): - """Disable Bitwarden""" - Bitwarden.disable() - return { - "status": 0, - "message": "Bitwarden disabled", - } - - -@router.post("/gitea/enable") -async def enable_gitea(): - """Enable Gitea""" - Gitea.enable() - return { - "status": 0, - "message": "Gitea enabled", - } - - -@router.post("/gitea/disable") -async def disable_gitea(): - """Disable Gitea""" - Gitea.disable() - return { - "status": 0, - "message": "Gitea disabled", - } - - -@router.get("/mailserver/dkim") -async def get_mailserver_dkim(): - """Get the DKIM record for the mailserver""" - domain = get_domain() - - dkim = get_dkim_key(domain, parse=False) - if dkim is None: - raise HTTPException(status_code=404, detail="DKIM record not found") - dkim = base64.b64encode(dkim.encode("utf-8")).decode("utf-8") - return dkim - - -@router.post("/nextcloud/enable") -async def enable_nextcloud(): - """Enable Nextcloud""" - Nextcloud.enable() - return { - "status": 0, - "message": "Nextcloud enabled", - } - - -@router.post("/nextcloud/disable") -async def disable_nextcloud(): - """Disable Nextcloud""" - Nextcloud.disable() - return { - "status": 0, - "message": "Nextcloud disabled", - } - - -@router.post("/ocserv/enable") -async def enable_ocserv(): - """Enable Ocserv""" - Ocserv.enable() - return { - "status": 0, - "message": "Ocserv enabled", - } - - -@router.post("/ocserv/disable") -async def disable_ocserv(): - """Disable Ocserv""" - Ocserv.disable() - return { - "status": 0, - "message": "Ocserv disabled", - } - - -@router.post("/pleroma/enable") -async def enable_pleroma(): - """Enable Pleroma""" - Pleroma.enable() - return { - "status": 0, - "message": "Pleroma enabled", - } - - -@router.post("/pleroma/disable") -async def disable_pleroma(): - """Disable Pleroma""" - Pleroma.disable() - return { - "status": 0, - "message": "Pleroma disabled", - } - - -@router.get("/restic/backup/list") -async def get_restic_backup_list(): - raise HTTPException( - status_code=410, - detail="This endpoint is deprecated, please use GraphQL API", - ) - - -@router.put("/restic/backup/create") -async def create_restic_backup(): - raise HTTPException( - status_code=410, - detail="This endpoint is deprecated, please use GraphQL API", - ) - - -@router.get("/restic/backup/status") -async def get_restic_backup_status(): - raise HTTPException( - status_code=410, - detail="This endpoint is deprecated, please use GraphQL API", - ) - - -@router.get("/restic/backup/reload") -async def reload_restic_backup(): - raise HTTPException( - status_code=410, - detail="This endpoint is deprecated, please use GraphQL API", - ) - - -class BackupRestoreInput(BaseModel): - backupId: str - - -@router.put("/restic/backup/restore") -async def restore_restic_backup(backup: BackupRestoreInput): - raise HTTPException( - status_code=410, - detail="This endpoint is deprecated, please use GraphQL API", - ) - - -class BackupConfigInput(BaseModel): - accountId: str - accountKey: str - bucket: str - - -@router.put("/restic/backblaze/config") -async def set_backblaze_config(backup_config: BackupConfigInput): - raise HTTPException( - status_code=410, - detail="This endpoint is deprecated, please use GraphQL API", - ) - - -@router.post("/ssh/enable") -async def rest_enable_ssh(): - """Enable SSH""" - enable_ssh() - return { - "status": 0, - "message": "SSH enabled", - } - - -@router.get("/ssh") -async def rest_get_ssh(): - """Get the SSH configuration""" - settings = get_ssh_settings() - return { - "enable": settings.enable, - "passwordAuthentication": settings.passwordAuthentication, - } - - -class SshConfigInput(BaseModel): - enable: Optional[bool] = None - passwordAuthentication: Optional[bool] = None - - -@router.put("/ssh") -async def rest_set_ssh(ssh_config: SshConfigInput): - """Set the SSH configuration""" - set_ssh_settings(ssh_config.enable, ssh_config.passwordAuthentication) - - return "SSH settings changed" - - -class SshKeyInput(BaseModel): - public_key: str - - -@router.put("/ssh/key/send", status_code=201) -async def rest_send_ssh_key(input: SshKeyInput): - """Send the SSH key""" - try: - create_ssh_key("root", input.public_key) - except KeyAlreadyExists as error: - raise HTTPException(status_code=409, detail="Key already exists") from error - except InvalidPublicKey as error: - raise HTTPException( - status_code=400, - detail="Invalid key type. Only ssh-ed25519 and ssh-rsa are supported", - ) from error - - return { - "status": 0, - "message": "SSH key sent", - } - - -@router.get("/ssh/keys/{username}") -async def rest_get_ssh_keys(username: str): - """Get the SSH keys for a user""" - user = get_user_by_username(username) - if user is None: - raise HTTPException(status_code=404, detail="User not found") - - return user.ssh_keys - - -@router.post("/ssh/keys/{username}", status_code=201) -async def rest_add_ssh_key(username: str, input: SshKeyInput): - try: - create_ssh_key(username, input.public_key) - except KeyAlreadyExists as error: - raise HTTPException(status_code=409, detail="Key already exists") from error - except InvalidPublicKey as error: - raise HTTPException( - status_code=400, - detail="Invalid key type. Only ssh-ed25519 and ssh-rsa are supported", - ) from error - except UserNotFound as error: - raise HTTPException(status_code=404, detail="User not found") from error - - return { - "message": "New SSH key successfully written", - } - - -@router.delete("/ssh/keys/{username}") -async def rest_delete_ssh_key(username: str, input: SshKeyInput): - try: - remove_ssh_key(username, input.public_key) - except KeyNotFound as error: - raise HTTPException(status_code=404, detail="Key not found") from error - except UserNotFound as error: - raise HTTPException(status_code=404, detail="User not found") from error - return {"message": "SSH key deleted"} diff --git a/selfprivacy_api/rest/system.py b/selfprivacy_api/rest/system.py deleted file mode 100644 index 9933fb3..0000000 --- a/selfprivacy_api/rest/system.py +++ /dev/null @@ -1,105 +0,0 @@ -from typing import Optional -from fastapi import APIRouter, Body, Depends, HTTPException -from pydantic import BaseModel - -from selfprivacy_api.dependencies import get_token_header - -import selfprivacy_api.actions.system as system_actions - -router = APIRouter( - prefix="/system", - tags=["system"], - dependencies=[Depends(get_token_header)], - responses={404: {"description": "Not found"}}, -) - - -@router.get("/configuration/timezone") -async def get_timezone(): - """Get the timezone of the server""" - return system_actions.get_timezone() - - -class ChangeTimezoneRequestBody(BaseModel): - """Change the timezone of the server""" - - timezone: str - - -@router.put("/configuration/timezone") -async def change_timezone(timezone: ChangeTimezoneRequestBody): - """Change the timezone of the server""" - try: - system_actions.change_timezone(timezone.timezone) - except system_actions.InvalidTimezone as e: - raise HTTPException(status_code=400, detail=str(e)) - return {"timezone": timezone.timezone} - - -@router.get("/configuration/autoUpgrade") -async def get_auto_upgrade_settings(): - """Get the auto-upgrade settings""" - return system_actions.get_auto_upgrade_settings().dict() - - -class AutoUpgradeSettings(BaseModel): - """Settings for auto-upgrading user data""" - - enable: Optional[bool] = None - allowReboot: Optional[bool] = None - - -@router.put("/configuration/autoUpgrade") -async def set_auto_upgrade_settings(settings: AutoUpgradeSettings): - """Set the auto-upgrade settings""" - system_actions.set_auto_upgrade_settings(settings.enable, settings.allowReboot) - return "Auto-upgrade settings changed" - - -@router.get("/configuration/apply") -async def apply_configuration(): - """Apply the configuration""" - return_code = system_actions.rebuild_system() - return return_code - - -@router.get("/configuration/rollback") -async def rollback_configuration(): - """Rollback the configuration""" - return_code = system_actions.rollback_system() - return return_code - - -@router.get("/configuration/upgrade") -async def upgrade_configuration(): - """Upgrade the configuration""" - return_code = system_actions.upgrade_system() - return return_code - - -@router.get("/reboot") -async def reboot_system(): - """Reboot the system""" - system_actions.reboot_system() - return "System reboot has started" - - -@router.get("/version") -async def get_system_version(): - """Get the system version""" - return {"system_version": system_actions.get_system_version()} - - -@router.get("/pythonVersion") -async def get_python_version(): - """Get the Python version""" - return system_actions.get_python_version() - - -@router.get("/configuration/pull") -async def pull_configuration(): - """Pull the configuration""" - action_result = system_actions.pull_repository_changes() - if action_result.status == 0: - return action_result.dict() - raise HTTPException(status_code=500, detail=action_result.dict()) diff --git a/selfprivacy_api/rest/users.py b/selfprivacy_api/rest/users.py deleted file mode 100644 index ab4c6c9..0000000 --- a/selfprivacy_api/rest/users.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Users management module""" -from typing import Optional -from fastapi import APIRouter, Body, Depends, HTTPException -from pydantic import BaseModel - -import selfprivacy_api.actions.users as users_actions - -from selfprivacy_api.dependencies import get_token_header - -router = APIRouter( - prefix="/users", - tags=["users"], - dependencies=[Depends(get_token_header)], - responses={404: {"description": "Not found"}}, -) - - -@router.get("") -async def get_users(withMainUser: bool = False): - """Get the list of users""" - users: list[users_actions.UserDataUser] = users_actions.get_users( - exclude_primary=not withMainUser, exclude_root=True - ) - - return [user.username for user in users] - - -class UserInput(BaseModel): - """User input""" - - username: str - password: str - - -@router.post("", status_code=201) -async def create_user(user: UserInput): - try: - users_actions.create_user(user.username, user.password) - except users_actions.PasswordIsEmpty as e: - raise HTTPException(status_code=400, detail=str(e)) - except users_actions.UsernameForbidden as e: - raise HTTPException(status_code=409, detail=str(e)) - except users_actions.UsernameNotAlphanumeric as e: - raise HTTPException(status_code=400, detail=str(e)) - except users_actions.UsernameTooLong as e: - raise HTTPException(status_code=400, detail=str(e)) - except users_actions.UserAlreadyExists as e: - raise HTTPException(status_code=409, detail=str(e)) - - return {"result": 0, "username": user.username} - - -@router.delete("/{username}") -async def delete_user(username: str): - try: - users_actions.delete_user(username) - except users_actions.UserNotFound as e: - raise HTTPException(status_code=404, detail=str(e)) - except users_actions.UserIsProtected as e: - raise HTTPException(status_code=400, detail=str(e)) - - return {"result": 0, "username": username} diff --git a/selfprivacy_api/services/bitwarden/__init__.py b/selfprivacy_api/services/bitwarden/__init__.py index 0d1dfdc..1590729 100644 --- a/selfprivacy_api/services/bitwarden/__init__.py +++ b/selfprivacy_api/services/bitwarden/__init__.py @@ -58,11 +58,6 @@ class Bitwarden(Service): def get_backup_description() -> str: return "Password database, encryption certificate and attachments." - @staticmethod - def is_enabled() -> bool: - with ReadUserData() as user_data: - return user_data.get("bitwarden", {}).get("enable", False) - @staticmethod def get_status() -> ServiceStatus: """ @@ -76,22 +71,6 @@ class Bitwarden(Service): """ return get_service_status("vaultwarden.service") - @staticmethod - def enable(): - """Enable Bitwarden service.""" - with WriteUserData() as user_data: - if "bitwarden" not in user_data: - user_data["bitwarden"] = {} - user_data["bitwarden"]["enable"] = True - - @staticmethod - def disable(): - """Disable Bitwarden service.""" - with WriteUserData() as user_data: - if "bitwarden" not in user_data: - user_data["bitwarden"] = {} - user_data["bitwarden"]["enable"] = False - @staticmethod def stop(): subprocess.run(["systemctl", "stop", "vaultwarden.service"]) diff --git a/selfprivacy_api/services/gitea/__init__.py b/selfprivacy_api/services/gitea/__init__.py index 08f223e..9b6f80f 100644 --- a/selfprivacy_api/services/gitea/__init__.py +++ b/selfprivacy_api/services/gitea/__init__.py @@ -54,11 +54,6 @@ class Gitea(Service): def get_backup_description() -> str: return "Git repositories, database and user data." - @staticmethod - def is_enabled() -> bool: - with ReadUserData() as user_data: - return user_data.get("gitea", {}).get("enable", False) - @staticmethod def get_status() -> ServiceStatus: """ @@ -71,22 +66,6 @@ class Gitea(Service): """ return get_service_status("gitea.service") - @staticmethod - def enable(): - """Enable Gitea service.""" - with WriteUserData() as user_data: - if "gitea" not in user_data: - user_data["gitea"] = {} - user_data["gitea"]["enable"] = True - - @staticmethod - def disable(): - """Disable Gitea service.""" - with WriteUserData() as user_data: - if "gitea" not in user_data: - user_data["gitea"] = {} - user_data["gitea"]["enable"] = False - @staticmethod def stop(): subprocess.run(["systemctl", "stop", "gitea.service"]) diff --git a/selfprivacy_api/services/jitsi/__init__.py b/selfprivacy_api/services/jitsi/__init__.py index fed6f33..d5677cc 100644 --- a/selfprivacy_api/services/jitsi/__init__.py +++ b/selfprivacy_api/services/jitsi/__init__.py @@ -55,33 +55,12 @@ class Jitsi(Service): def get_backup_description() -> str: return "Secrets that are used to encrypt the communication." - @staticmethod - def is_enabled() -> bool: - with ReadUserData() as user_data: - return user_data.get("jitsi", {}).get("enable", False) - @staticmethod def get_status() -> ServiceStatus: return get_service_status_from_several_units( ["jitsi-videobridge.service", "jicofo.service"] ) - @staticmethod - def enable(): - """Enable Jitsi service.""" - with WriteUserData() as user_data: - if "jitsi" not in user_data: - user_data["jitsi"] = {} - user_data["jitsi"]["enable"] = True - - @staticmethod - def disable(): - """Disable Gitea service.""" - with WriteUserData() as user_data: - if "jitsi" not in user_data: - user_data["jitsi"] = {} - user_data["jitsi"]["enable"] = False - @staticmethod def stop(): subprocess.run( diff --git a/selfprivacy_api/services/nextcloud/__init__.py b/selfprivacy_api/services/nextcloud/__init__.py index 1703478..0da6dd9 100644 --- a/selfprivacy_api/services/nextcloud/__init__.py +++ b/selfprivacy_api/services/nextcloud/__init__.py @@ -53,11 +53,6 @@ class Nextcloud(Service): def get_backup_description() -> str: return "All the files and other data stored in Nextcloud." - @staticmethod - def is_enabled() -> bool: - with ReadUserData() as user_data: - return user_data.get("nextcloud", {}).get("enable", False) - @staticmethod def get_status() -> ServiceStatus: """ @@ -71,22 +66,6 @@ class Nextcloud(Service): """ return get_service_status("phpfpm-nextcloud.service") - @staticmethod - def enable(): - """Enable Nextcloud service.""" - with WriteUserData() as user_data: - if "nextcloud" not in user_data: - user_data["nextcloud"] = {} - user_data["nextcloud"]["enable"] = True - - @staticmethod - def disable(): - """Disable Nextcloud service.""" - with WriteUserData() as user_data: - if "nextcloud" not in user_data: - user_data["nextcloud"] = {} - user_data["nextcloud"]["enable"] = False - @staticmethod def stop(): """Stop Nextcloud service.""" diff --git a/selfprivacy_api/services/ocserv/__init__.py b/selfprivacy_api/services/ocserv/__init__.py index d9d59a0..a28358d 100644 --- a/selfprivacy_api/services/ocserv/__init__.py +++ b/selfprivacy_api/services/ocserv/__init__.py @@ -51,29 +51,10 @@ class Ocserv(Service): def get_backup_description() -> str: return "Nothing to backup." - @staticmethod - def is_enabled() -> bool: - with ReadUserData() as user_data: - return user_data.get("ocserv", {}).get("enable", False) - @staticmethod def get_status() -> ServiceStatus: return get_service_status("ocserv.service") - @staticmethod - def enable(): - with WriteUserData() as user_data: - if "ocserv" not in user_data: - user_data["ocserv"] = {} - user_data["ocserv"]["enable"] = True - - @staticmethod - def disable(): - with WriteUserData() as user_data: - if "ocserv" not in user_data: - user_data["ocserv"] = {} - user_data["ocserv"]["enable"] = False - @staticmethod def stop(): subprocess.run(["systemctl", "stop", "ocserv.service"], check=False) diff --git a/selfprivacy_api/services/pleroma/__init__.py b/selfprivacy_api/services/pleroma/__init__.py index b2540d8..1aae50e 100644 --- a/selfprivacy_api/services/pleroma/__init__.py +++ b/selfprivacy_api/services/pleroma/__init__.py @@ -50,29 +50,10 @@ class Pleroma(Service): def get_backup_description() -> str: return "Your Pleroma accounts, posts and media." - @staticmethod - def is_enabled() -> bool: - with ReadUserData() as user_data: - return user_data.get("pleroma", {}).get("enable", False) - @staticmethod def get_status() -> ServiceStatus: return get_service_status("pleroma.service") - @staticmethod - def enable(): - with WriteUserData() as user_data: - if "pleroma" not in user_data: - user_data["pleroma"] = {} - user_data["pleroma"]["enable"] = True - - @staticmethod - def disable(): - with WriteUserData() as user_data: - if "pleroma" not in user_data: - user_data["pleroma"] = {} - user_data["pleroma"]["enable"] = False - @staticmethod def stop(): subprocess.run(["systemctl", "stop", "pleroma.service"]) diff --git a/selfprivacy_api/services/service.py b/selfprivacy_api/services/service.py index 8446133..b44f3a9 100644 --- a/selfprivacy_api/services/service.py +++ b/selfprivacy_api/services/service.py @@ -12,6 +12,7 @@ from selfprivacy_api.services.generic_size_counter import get_storage_usage from selfprivacy_api.services.owned_path import OwnedPath from selfprivacy_api import utils from selfprivacy_api.utils.waitloop import wait_until_true +from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain DEFAULT_START_STOP_TIMEOUT = 5 * 60 @@ -125,11 +126,17 @@ class Service(ABC): """ pass - @staticmethod - @abstractmethod - def is_enabled() -> bool: - """`True` if the service is enabled.""" - pass + @classmethod + def is_enabled(cls) -> bool: + """ + `True` if the service is enabled. + `False` if it is not enabled or not defined in file + If there is nothing in the file, this is equivalent to False + because NixOS won't enable it then. + """ + name = cls.get_id() + with ReadUserData() as user_data: + return user_data.get("modules", {}).get(name, {}).get("enable", False) @staticmethod @abstractmethod @@ -137,17 +144,25 @@ class Service(ABC): """The status of the service, reported by systemd.""" pass - @staticmethod - @abstractmethod - def enable(): - """Enable the service. Usually this means enabling systemd unit.""" - pass + @classmethod + def _set_enable(cls, enable: bool): + name = cls.get_id() + with WriteUserData() as user_data: + if "modules" not in user_data: + user_data["modules"] = {} + if name not in user_data["modules"]: + user_data["modules"][name] = {} + user_data["modules"][name]["enable"] = enable - @staticmethod - @abstractmethod - def disable(): + @classmethod + def enable(cls): + """Enable the service. Usually this means enabling systemd unit.""" + cls._set_enable(True) + + @classmethod + def disable(cls): """Disable the service. Usually this means disabling systemd unit.""" - pass + cls._set_enable(False) @staticmethod @abstractmethod @@ -247,6 +262,8 @@ class Service(ABC): @abstractmethod def move_to_volume(self, volume: BlockDevice) -> Job: + """Cannot raise errors. + Returns errors as an errored out Job instead.""" pass @classmethod diff --git a/selfprivacy_api/services/test_service/__init__.py b/selfprivacy_api/services/test_service/__init__.py index 187a1c6..1e315f5 100644 --- a/selfprivacy_api/services/test_service/__init__.py +++ b/selfprivacy_api/services/test_service/__init__.py @@ -8,9 +8,10 @@ from os import path # from enum import Enum -from selfprivacy_api.jobs import Job +from selfprivacy_api.jobs import Job, Jobs, JobStatus from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus from selfprivacy_api.utils.block_devices import BlockDevice +from selfprivacy_api.services.generic_service_mover import move_service, FolderMoveNames import selfprivacy_api.utils.network as network_utils from selfprivacy_api.services.test_service.icon import BITWARDEN_ICON @@ -22,16 +23,19 @@ class DummyService(Service): """A test service""" folders: List[str] = [] - startstop_delay = 0 + startstop_delay = 0.0 backuppable = True + movable = True + # if False, we try to actually move + simulate_moving = True + drive = "sda1" def __init_subclass__(cls, folders: List[str]): cls.folders = folders def __init__(self): super().__init__() - status_file = self.status_file() - with open(status_file, "w") as file: + with open(self.status_file(), "w") as file: file.write(ServiceStatus.ACTIVE.value) @staticmethod @@ -61,9 +65,9 @@ class DummyService(Service): domain = "test.com" return f"https://password.{domain}" - @staticmethod - def is_movable() -> bool: - return True + @classmethod + def is_movable(cls) -> bool: + return cls.movable @staticmethod def is_required() -> bool: @@ -73,10 +77,6 @@ class DummyService(Service): def get_backup_description() -> str: return "How did we get here?" - @staticmethod - def is_enabled() -> bool: - return True - @classmethod def status_file(cls) -> str: dir = cls.folders[0] @@ -116,22 +116,30 @@ class DummyService(Service): we can only set it up dynamically for tests via a classmethod""" cls.backuppable = new_value + @classmethod + def set_movable(cls, new_value: bool) -> None: + """For tests: because is_movale is static, + we can only set it up dynamically for tests via a classmethod""" + cls.movable = new_value + @classmethod def can_be_backed_up(cls) -> bool: """`True` if the service can be backed up.""" return cls.backuppable @classmethod - def enable(cls): - pass + def set_delay(cls, new_delay_sec: float) -> None: + cls.startstop_delay = new_delay_sec @classmethod - def disable(cls, delay): - pass + def set_drive(cls, new_drive: str) -> None: + cls.drive = new_drive @classmethod - def set_delay(cls, new_delay): - cls.startstop_delay = new_delay + def set_simulated_moves(cls, enabled: bool) -> None: + """If True, this service will not actually call moving code + when moved""" + cls.simulate_moving = enabled @classmethod def stop(cls): @@ -169,9 +177,9 @@ class DummyService(Service): storage_usage = 0 return storage_usage - @staticmethod - def get_drive() -> str: - return "sda1" + @classmethod + def get_drive(cls) -> str: + return cls.drive @classmethod def get_folders(cls) -> List[str]: @@ -198,4 +206,22 @@ class DummyService(Service): ] def move_to_volume(self, volume: BlockDevice) -> Job: - pass + job = Jobs.add( + type_id=f"services.{self.get_id()}.move", + name=f"Move {self.get_display_name()}", + description=f"Moving {self.get_display_name()} data to {volume.name}", + ) + if self.simulate_moving is False: + # completely generic code, TODO: make it the default impl. + move_service( + self, + volume, + job, + FolderMoveNames.default_foldermoves(self), + self.get_id(), + ) + else: + Jobs.update(job, status=JobStatus.FINISHED) + + self.set_drive(volume.name) + return job diff --git a/selfprivacy_api/utils/__init__.py b/selfprivacy_api/utils/__init__.py index 40ed5b6..08bc61f 100644 --- a/selfprivacy_api/utils/__init__.py +++ b/selfprivacy_api/utils/__init__.py @@ -6,12 +6,14 @@ import json import os import subprocess import portalocker +import typing USERDATA_FILE = "/etc/nixos/userdata/userdata.json" TOKENS_FILE = "/etc/nixos/userdata/tokens.json" JOBS_FILE = "/etc/nixos/userdata/jobs.json" DOMAIN_FILE = "/var/domain" +DKIM_DIR = "/var/dkim/" class UserDataFiles(Enum): @@ -166,26 +168,31 @@ def parse_date(date_str: str) -> datetime.datetime: raise ValueError("Invalid date string") -def get_dkim_key(domain, parse=True): +def parse_dkim(dkim: str) -> str: + # extract key from file + dkim = dkim.split("(")[1] + dkim = dkim.split(")")[0] + # replace all quotes with nothing + dkim = dkim.replace('"', "") + # trim whitespace, remove newlines and tabs + dkim = dkim.strip() + dkim = dkim.replace("\n", "") + dkim = dkim.replace("\t", "") + # remove all redundant spaces + dkim = " ".join(dkim.split()) + return dkim + + +def get_dkim_key(domain: str, parse: bool = True) -> typing.Optional[str]: """Get DKIM key from /var/dkim/.selector.txt""" - if os.path.exists("/var/dkim/" + domain + ".selector.txt"): - cat_process = subprocess.Popen( - ["cat", "/var/dkim/" + domain + ".selector.txt"], stdout=subprocess.PIPE - ) - dkim = cat_process.communicate()[0] - if parse: - # Extract key from file - dkim = dkim.split(b"(")[1] - dkim = dkim.split(b")")[0] - # Replace all quotes with nothing - dkim = dkim.replace(b'"', b"") - # Trim whitespace, remove newlines and tabs - dkim = dkim.strip() - dkim = dkim.replace(b"\n", b"") - dkim = dkim.replace(b"\t", b"") - # Remove all redundant spaces - dkim = b" ".join(dkim.split()) - return str(dkim, "utf-8") + + dkim_path = os.path.join(DKIM_DIR, domain + ".selector.txt") + if os.path.exists(dkim_path): + with open(dkim_path, encoding="utf-8") as dkim_file: + dkim = dkim_file.read() + if parse: + dkim = parse_dkim(dkim) + return dkim return None diff --git a/selfprivacy_api/utils/block_devices.py b/selfprivacy_api/utils/block_devices.py index 83fc28f..ab3794d 100644 --- a/selfprivacy_api/utils/block_devices.py +++ b/selfprivacy_api/utils/block_devices.py @@ -1,4 +1,5 @@ -"""Wrapper for block device functions.""" +"""A block device API wrapping lsblk""" +from __future__ import annotations import subprocess import json import typing @@ -11,6 +12,7 @@ def get_block_device(device_name): """ Return a block device by name. """ + # TODO: remove the function and related tests: dublicated by singleton lsblk_output = subprocess.check_output( [ "lsblk", @@ -43,22 +45,37 @@ class BlockDevice: A block device. """ - def __init__(self, block_device): - self.name = block_device["name"] - self.path = block_device["path"] - self.fsavail = str(block_device["fsavail"]) - self.fssize = str(block_device["fssize"]) - self.fstype = block_device["fstype"] - self.fsused = str(block_device["fsused"]) - self.mountpoints = block_device["mountpoints"] - self.label = block_device["label"] - self.uuid = block_device["uuid"] - self.size = str(block_device["size"]) - self.model = block_device["model"] - self.serial = block_device["serial"] - self.type = block_device["type"] + def __init__(self, device_dict: dict): + self.update_from_dict(device_dict) + + def update_from_dict(self, device_dict: dict): + self.name = device_dict["name"] + self.path = device_dict["path"] + self.fsavail = str(device_dict["fsavail"]) + self.fssize = str(device_dict["fssize"]) + self.fstype = device_dict["fstype"] + self.fsused = str(device_dict["fsused"]) + self.mountpoints = device_dict["mountpoints"] + self.label = device_dict["label"] + self.uuid = device_dict["uuid"] + self.size = str(device_dict["size"]) + self.model = device_dict["model"] + self.serial = device_dict["serial"] + self.type = device_dict["type"] self.locked = False + self.children: typing.List[BlockDevice] = [] + if "children" in device_dict.keys(): + for child in device_dict["children"]: + self.children.append(BlockDevice(child)) + + def all_children(self) -> typing.List[BlockDevice]: + result = [] + for child in self.children: + result.extend(child.all_children()) + result.append(child) + return result + def __str__(self): return self.name @@ -82,17 +99,7 @@ class BlockDevice: Update current data and return a dictionary of stats. """ device = get_block_device(self.name) - self.fsavail = str(device["fsavail"]) - self.fssize = str(device["fssize"]) - self.fstype = device["fstype"] - self.fsused = str(device["fsused"]) - self.mountpoints = device["mountpoints"] - self.label = device["label"] - self.uuid = device["uuid"] - self.size = str(device["size"]) - self.model = device["model"] - self.serial = device["serial"] - self.type = device["type"] + self.update_from_dict(device) return { "name": self.name, @@ -110,6 +117,14 @@ class BlockDevice: "type": self.type, } + def is_usable_partition(self): + # Ignore devices with type "rom" + if self.type == "rom": + return False + if self.fstype == "ext4": + return True + return False + def resize(self): """ Resize the block device. @@ -165,41 +180,16 @@ class BlockDevices(metaclass=SingletonMetaclass): """ Update the list of block devices. """ - devices = [] - lsblk_output = subprocess.check_output( - [ - "lsblk", - "-J", - "-b", - "-o", - "NAME,PATH,FSAVAIL,FSSIZE,FSTYPE,FSUSED,MOUNTPOINTS,LABEL,UUID,SIZE,MODEL,SERIAL,TYPE", - ] - ) - lsblk_output = lsblk_output.decode("utf-8") - lsblk_output = json.loads(lsblk_output) - for device in lsblk_output["blockdevices"]: - # Ignore devices with type "rom" - if device["type"] == "rom": - continue - # Ignore iso9660 devices - if device["fstype"] == "iso9660": - continue - if device["fstype"] is None: - if "children" in device: - for child in device["children"]: - if child["fstype"] == "ext4": - device = child - break - devices.append(device) - # Add new devices and delete non-existent devices + devices = BlockDevices.lsblk_devices() + + children = [] for device in devices: - if device["name"] not in [ - block_device.name for block_device in self.block_devices - ]: - self.block_devices.append(BlockDevice(device)) - for block_device in self.block_devices: - if block_device.name not in [device["name"] for device in devices]: - self.block_devices.remove(block_device) + children.extend(device.all_children()) + devices.extend(children) + + valid_devices = [device for device in devices if device.is_usable_partition()] + + self.block_devices = valid_devices def get_block_device(self, name: str) -> typing.Optional[BlockDevice]: """ @@ -236,3 +226,25 @@ class BlockDevices(metaclass=SingletonMetaclass): if "/" in block_device.mountpoints: return block_device raise RuntimeError("No root block device found") + + @staticmethod + def lsblk_device_dicts() -> typing.List[dict]: + lsblk_output_bytes = subprocess.check_output( + [ + "lsblk", + "-J", + "-b", + "-o", + "NAME,PATH,FSAVAIL,FSSIZE,FSTYPE,FSUSED,MOUNTPOINTS,LABEL,UUID,SIZE,MODEL,SERIAL,TYPE", + ] + ) + lsblk_output = lsblk_output_bytes.decode("utf-8") + return json.loads(lsblk_output)["blockdevices"] + + @staticmethod + def lsblk_devices() -> typing.List[BlockDevice]: + devices = [] + for device in BlockDevices.lsblk_device_dicts(): + devices.append(device) + + return [BlockDevice(device) for device in devices] diff --git a/setup.py b/setup.py index 93637ff..36aa68e 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="selfprivacy_api", - version="2.4.3", + version="3.0.0", packages=find_packages(), scripts=[ "selfprivacy_api/app.py", diff --git a/tests/common.py b/tests/common.py index 5199899..8061721 100644 --- a/tests/common.py +++ b/tests/common.py @@ -67,6 +67,10 @@ def generate_backup_query(query_array): return "query TestBackup {\n backup {" + "\n".join(query_array) + "}\n}" +def generate_service_query(query_array): + return "query TestService {\n services {" + "\n".join(query_array) + "}\n}" + + def mnemonic_to_hex(mnemonic): return Mnemonic(language="english").to_entropy(mnemonic).hex() diff --git a/tests/conftest.py b/tests/conftest.py index f058997..fddd32f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,12 +3,19 @@ # pylint: disable=unused-argument import os import pytest -from os import path - -from fastapi.testclient import TestClient -import os.path as path import datetime +from os import path +from os import makedirs +from typing import Generator +from fastapi.testclient import TestClient + +from selfprivacy_api.utils.huey import huey + +import selfprivacy_api.services as services +from selfprivacy_api.services import get_service_by_id, Service +from selfprivacy_api.services.test_service import DummyService + from selfprivacy_api.models.tokens.token import Token from selfprivacy_api.repositories.tokens.json_tokens_repository import ( JsonTokensRepository, @@ -19,6 +26,9 @@ from selfprivacy_api.repositories.tokens.redis_tokens_repository import ( from tests.common import read_json +TESTFILE_BODY = "testytest!" +TESTFILE_2_BODY = "testissimo!" + EMPTY_TOKENS_JSON = ' {"tokens": []}' @@ -147,3 +157,49 @@ def wrong_auth_client(tokens_file, huey_database, jobs_file): client = TestClient(app) client.headers.update({"Authorization": "Bearer WRONG_TOKEN"}) return client + + +@pytest.fixture() +def raw_dummy_service(tmpdir): + dirnames = ["test_service", "also_test_service"] + service_dirs = [] + for d in dirnames: + service_dir = path.join(tmpdir, d) + makedirs(service_dir) + service_dirs.append(service_dir) + + testfile_path_1 = path.join(service_dirs[0], "testfile.txt") + with open(testfile_path_1, "w") as file: + file.write(TESTFILE_BODY) + + testfile_path_2 = path.join(service_dirs[1], "testfile2.txt") + with open(testfile_path_2, "w") as file: + file.write(TESTFILE_2_BODY) + + # we need this to not change get_folders() much + class TestDummyService(DummyService, folders=service_dirs): + pass + + service = TestDummyService() + # assert pickle.dumps(service) is not None + return service + + +@pytest.fixture() +def dummy_service( + tmpdir, raw_dummy_service, generic_userdata +) -> Generator[Service, None, None]: + service = raw_dummy_service + + # register our service + services.services.append(service) + + huey.immediate = True + assert huey.immediate is True + + assert get_service_by_id(service.get_id()) is not None + service.enable() + yield service + + # cleanup because apparently it matters wrt tasks + services.services.remove(service) diff --git a/tests/test_rest_endpoints/test_system/domain b/tests/data/domain similarity index 100% rename from tests/test_rest_endpoints/test_system/domain rename to tests/data/domain diff --git a/tests/data/turned_on.json b/tests/data/turned_on.json index c6b758b..1b6219d 100644 --- a/tests/data/turned_on.json +++ b/tests/data/turned_on.json @@ -1,60 +1,55 @@ { - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": true - }, + "api": {"token": "TEST_TOKEN", "enableSwagger": false}, "databasePassword": "PASSWORD", "domain": "test.tld", "hashedMasterPassword": "HASHED_PASSWORD", "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, "resticPassword": "PASS", "ssh": { "enable": true, "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] + "rootKeys": ["ssh-ed25519 KEY test@pc"] }, "username": "tester", - "gitea": { - "enable": true - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "jitsi": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, + "autoUpgrade": {"enable": true, "allowReboot": true}, + "useBinds": true, "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" + "sshKeys": ["ssh-rsa KEY test@pc"], + "dns": {"provider": "CLOUDFLARE", "apiKey": "TOKEN"}, + "server": {"provider": "HETZNER"}, + "modules": { + "bitwarden": {"enable": true}, + "gitea": {"enable": true}, + "ocserv": {"enable": true}, + "pleroma": {"enable": true}, + "jitsi": {"enable": true}, + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + } }, "backup": { "provider": "BACKBLAZE", "accountId": "ID", "accountKey": "KEY", "bucket": "selfprivacy" - } + }, + "users": [ + { + "username": "user1", + "hashedPassword": "HASHED_PASSWORD_1", + "sshKeys": ["ssh-rsa KEY user1@pc"] + }, + { + "username": "user2", + "hashedPassword": "HASHED_PASSWORD_2", + "sshKeys": ["ssh-rsa KEY user2@pc"] + }, + { + "username": "user3", + "hashedPassword": "HASHED_PASSWORD_3", + "sshKeys": ["ssh-rsa KEY user3@pc"] + } + ] } diff --git a/tests/test_graphql/test_backup.py b/tests/test_backup.py similarity index 95% rename from tests/test_graphql/test_backup.py rename to tests/test_backup.py index b66a90d..036dd42 100644 --- a/tests/test_graphql/test_backup.py +++ b/tests/test_backup.py @@ -2,7 +2,6 @@ import pytest import os import os.path as path -from os import makedirs from os import remove from os import listdir from os import urandom @@ -13,7 +12,10 @@ import tempfile from selfprivacy_api.utils.huey import huey -import selfprivacy_api.services as services +import tempfile + +from selfprivacy_api.utils.huey import huey + from selfprivacy_api.services import Service, get_all_services from selfprivacy_api.services import get_service_by_id from selfprivacy_api.services.service import ServiceStatus @@ -46,13 +48,13 @@ from selfprivacy_api.backup.tasks import ( from selfprivacy_api.backup.storage import Storage -TESTFILE_BODY = "testytest!" -TESTFILE_2_BODY = "testissimo!" REPO_NAME = "test_backup" +REPOFILE_NAME = "totallyunrelated" + def prepare_localfile_backups(temp_dir): - test_repo_path = path.join(temp_dir, "totallyunrelated") + test_repo_path = path.join(temp_dir, REPOFILE_NAME) assert not path.exists(test_repo_path) Backups.set_localfile_repo(test_repo_path) @@ -67,16 +69,24 @@ def backups_local(tmpdir): @pytest.fixture(scope="function") def backups(tmpdir): - # for those tests that are supposed to pass with any repo + """ + For those tests that are supposed to pass with + both local and cloud repos + """ + + # Sometimes this is false. Idk why. + huey.immediate = True + assert huey.immediate is True + Backups.reset() if BACKUP_PROVIDER_ENVS["kind"] in os.environ.keys(): Backups.set_provider_from_envs() else: prepare_localfile_backups(tmpdir) Jobs.reset() - # assert not repo_path Backups.init_repo() + assert Backups.provider().location == str(tmpdir) + "/" + REPOFILE_NAME yield Backups.erase_repo() @@ -86,49 +96,6 @@ def backups_backblaze(generic_userdata): Backups.reset(reset_json=False) -@pytest.fixture() -def raw_dummy_service(tmpdir): - dirnames = ["test_service", "also_test_service"] - service_dirs = [] - for d in dirnames: - service_dir = path.join(tmpdir, d) - makedirs(service_dir) - service_dirs.append(service_dir) - - testfile_path_1 = path.join(service_dirs[0], "testfile.txt") - with open(testfile_path_1, "w") as file: - file.write(TESTFILE_BODY) - - testfile_path_2 = path.join(service_dirs[1], "testfile2.txt") - with open(testfile_path_2, "w") as file: - file.write(TESTFILE_2_BODY) - - # we need this to not change get_folders() much - class TestDummyService(DummyService, folders=service_dirs): - pass - - service = TestDummyService() - return service - - -@pytest.fixture() -def dummy_service(tmpdir, backups, raw_dummy_service) -> Service: - service = raw_dummy_service - - # register our service - services.services.append(service) - - # make sure we are in immediate mode because this thing is non pickleable to store on queue. - huey.immediate = True - assert huey.immediate is True - - assert get_service_by_id(service.get_id()) is not None - yield service - - # cleanup because apparently it matters wrt tasks - services.services.remove(service) - - @pytest.fixture() def memory_backup() -> AbstractBackupProvider: ProviderClass = providers.get_provider(BackupProvider.MEMORY) @@ -777,7 +744,7 @@ def simulated_service_stopping_delay(request) -> float: def test_backup_service_task(backups, dummy_service, simulated_service_stopping_delay): dummy_service.set_delay(simulated_service_stopping_delay) - handle = start_backup(dummy_service) + handle = start_backup(dummy_service.get_id()) handle(blocking=True) snaps = Backups.get_snapshots(dummy_service) @@ -822,7 +789,7 @@ def test_backup_larger_file(backups, dummy_service): mega = 2**20 make_large_file(dir, 100 * mega) - handle = start_backup(dummy_service) + handle = start_backup(dummy_service.get_id()) handle(blocking=True) # results will be slightly different on different machines. if someone has troubles with it on their machine, consider dropping this test. diff --git a/tests/test_block_device_utils.py b/tests/test_block_device_utils.py index f821e96..7a85c50 100644 --- a/tests/test_block_device_utils.py +++ b/tests/test_block_device_utils.py @@ -416,32 +416,37 @@ def lsblk_full_mock(mocker): def test_get_block_devices(lsblk_full_mock, authorized_client): block_devices = BlockDevices().get_block_devices() assert len(block_devices) == 2 - assert block_devices[0].name == "sda1" - assert block_devices[0].path == "/dev/sda1" - assert block_devices[0].fsavail == "4605702144" - assert block_devices[0].fssize == "19814920192" - assert block_devices[0].fstype == "ext4" - assert block_devices[0].fsused == "14353719296" - assert block_devices[0].mountpoints == ["/nix/store", "/"] - assert block_devices[0].label is None - assert block_devices[0].uuid == "ec80c004-baec-4a2c-851d-0e1807135511" - assert block_devices[0].size == "20210236928" - assert block_devices[0].model is None - assert block_devices[0].serial is None - assert block_devices[0].type == "part" - assert block_devices[1].name == "sdb" - assert block_devices[1].path == "/dev/sdb" - assert block_devices[1].fsavail == "11888545792" - assert block_devices[1].fssize == "12573614080" - assert block_devices[1].fstype == "ext4" - assert block_devices[1].fsused == "24047616" - assert block_devices[1].mountpoints == ["/volumes/sdb"] - assert block_devices[1].label is None - assert block_devices[1].uuid == "fa9d0026-ee23-4047-b8b1-297ae16fa751" - assert block_devices[1].size == "12884901888" - assert block_devices[1].model == "Volume" - assert block_devices[1].serial == "21378102" - assert block_devices[1].type == "disk" + devices_by_name = {device.name: device for device in block_devices} + sda1 = devices_by_name["sda1"] + sdb = devices_by_name["sdb"] + + assert sda1.name == "sda1" + assert sda1.path == "/dev/sda1" + assert sda1.fsavail == "4605702144" + assert sda1.fssize == "19814920192" + assert sda1.fstype == "ext4" + assert sda1.fsused == "14353719296" + assert sda1.mountpoints == ["/nix/store", "/"] + assert sda1.label is None + assert sda1.uuid == "ec80c004-baec-4a2c-851d-0e1807135511" + assert sda1.size == "20210236928" + assert sda1.model is None + assert sda1.serial is None + assert sda1.type == "part" + + assert sdb.name == "sdb" + assert sdb.path == "/dev/sdb" + assert sdb.fsavail == "11888545792" + assert sdb.fssize == "12573614080" + assert sdb.fstype == "ext4" + assert sdb.fsused == "24047616" + assert sdb.mountpoints == ["/volumes/sdb"] + assert sdb.label is None + assert sdb.uuid == "fa9d0026-ee23-4047-b8b1-297ae16fa751" + assert sdb.size == "12884901888" + assert sdb.model == "Volume" + assert sdb.serial == "21378102" + assert sdb.type == "disk" def test_get_block_device(lsblk_full_mock, authorized_client): @@ -506,3 +511,30 @@ def test_get_root_block_device(lsblk_full_mock, authorized_client): assert block_device.model is None assert block_device.serial is None assert block_device.type == "part" + + +# Unassuming sanity check, yes this did fail +def test_get_real_devices(): + block_devices = BlockDevices().get_block_devices() + + assert block_devices is not None + assert len(block_devices) > 0 + + +# Unassuming sanity check +def test_get_real_root_device(): + devices = BlockDevices().get_block_devices() + try: + block_device = BlockDevices().get_root_block_device() + except Exception as e: + raise Exception("cannot get root device:", e, "devices found:", devices) + assert block_device is not None + assert block_device.name is not None + assert block_device.name != "" + + +def test_get_real_root_device_raw(authorized_client): + block_device = BlockDevices().get_root_block_device() + assert block_device is not None + assert block_device.name is not None + assert block_device.name != "" diff --git a/tests/test_common.py b/tests/test_common.py index e5d3f62..7dd3652 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,6 +1,5 @@ # pylint: disable=redefined-outer-name # pylint: disable=unused-argument -import json import os import pytest diff --git a/tests/test_dkim.py b/tests/test_dkim.py new file mode 100644 index 0000000..949bb19 --- /dev/null +++ b/tests/test_dkim.py @@ -0,0 +1,60 @@ +import pytest + +import os +from os import path +from tests.conftest import global_data_dir + +from selfprivacy_api.utils import get_dkim_key, get_domain + +############################################################################### + +DKIM_FILE_CONTENT = b'selector._domainkey\tIN\tTXT\t( "v=DKIM1; k=rsa; "\n\t "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNn/IhEz1SxgHxxxI8vlPYC2dNueiLe1GC4SYz8uHimC8SDkMvAwm7rqi2SimbFgGB5nccCNOqCkrIqJTCB9vufqBnVKAjshHqpOr5hk4JJ1T/AGQKWinstmDbfTLPYTbU8ijZrwwGeqQLlnXR5nSN0GB9GazheA9zaPsT6PV+aQIDAQAB" ) ; ----- DKIM key selector for test-domain.tld\n' + + +@pytest.fixture +def dkim_file(mocker, domain_file, tmpdir): + domain = get_domain() + assert domain is not None + assert domain != "" + + filename = domain + ".selector.txt" + dkim_path = path.join(tmpdir, filename) + + with open(dkim_path, "wb") as file: + file.write(DKIM_FILE_CONTENT) + + mocker.patch("selfprivacy_api.utils.DKIM_DIR", tmpdir) + return dkim_path + + +@pytest.fixture +def domain_file(mocker): + # TODO: move to conftest. Challenge: it does not behave with "/" like pytest datadir does + domain_path = path.join(global_data_dir(), "domain") + mocker.patch("selfprivacy_api.utils.DOMAIN_FILE", domain_path) + return domain_path + + +@pytest.fixture +def no_dkim_file(dkim_file): + os.remove(dkim_file) + assert path.exists(dkim_file) is False + return dkim_file + + +############################################################################### + + +def test_get_dkim_key(domain_file, dkim_file): + """Test DKIM key""" + dkim_key = get_dkim_key("test-domain.tld") + assert ( + dkim_key + == "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNn/IhEz1SxgHxxxI8vlPYC2dNueiLe1GC4SYz8uHimC8SDkMvAwm7rqi2SimbFgGB5nccCNOqCkrIqJTCB9vufqBnVKAjshHqpOr5hk4JJ1T/AGQKWinstmDbfTLPYTbU8ijZrwwGeqQLlnXR5nSN0GB9GazheA9zaPsT6PV+aQIDAQAB" + ) + + +def test_no_dkim_key(domain_file, no_dkim_file): + """Test no DKIM key""" + dkim_key = get_dkim_key("test-domain.tld") + assert dkim_key is None diff --git a/tests/test_graphql/api_common.py b/tests/test_graphql/api_common.py deleted file mode 100644 index 4e4aec2..0000000 --- a/tests/test_graphql/api_common.py +++ /dev/null @@ -1,89 +0,0 @@ -from tests.common import generate_api_query -from tests.conftest import TOKENS_FILE_CONTENTS, DEVICE_WE_AUTH_TESTS_WITH - -ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] - - -def assert_ok(response, request): - data = assert_data(response) - assert data[request]["success"] is True - assert data[request]["message"] is not None - assert data[request]["code"] == 200 - - -def assert_errorcode(response, request, code): - data = assert_data(response) - assert data[request]["success"] is False - assert data[request]["message"] is not None - assert data[request]["code"] == code - - -def assert_empty(response): - assert response.status_code == 200 - assert response.json().get("data") is None - - -def assert_data(response): - assert response.status_code == 200 - data = response.json().get("data") - assert data is not None - assert "api" in data.keys() - return data["api"] - - -API_DEVICES_QUERY = """ -devices { - creationDate - isCaller - name -} -""" - - -def request_devices(client): - return client.post( - "/graphql", - json={"query": generate_api_query([API_DEVICES_QUERY])}, - ) - - -def graphql_get_devices(client): - response = request_devices(client) - data = assert_data(response) - devices = data["devices"] - assert devices is not None - return devices - - -def set_client_token(client, token): - client.headers.update({"Authorization": "Bearer " + token}) - - -def assert_token_valid(client, token): - set_client_token(client, token) - assert graphql_get_devices(client) is not None - - -def assert_same(graphql_devices, abstract_devices): - """Orderless comparison""" - assert len(graphql_devices) == len(abstract_devices) - for original_device in abstract_devices: - assert original_device["name"] in [device["name"] for device in graphql_devices] - for device in graphql_devices: - if device["name"] == original_device["name"]: - assert device["creationDate"] == original_device["date"].isoformat() - - -def assert_original(client): - devices = graphql_get_devices(client) - assert_original_devices(devices) - - -def assert_original_devices(devices): - assert_same(devices, ORIGINAL_DEVICES) - - for device in devices: - if device["name"] == DEVICE_WE_AUTH_TESTS_WITH["name"]: - assert device["isCaller"] is True - else: - assert device["isCaller"] is False diff --git a/tests/test_graphql/common.py b/tests/test_graphql/common.py index d473433..5e6dc04 100644 --- a/tests/test_graphql/common.py +++ b/tests/test_graphql/common.py @@ -4,18 +4,20 @@ from tests.conftest import TOKENS_FILE_CONTENTS, DEVICE_WE_AUTH_TESTS_WITH ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] -def assert_ok(response, request): - data = assert_data(response) - data[request]["success"] is True - data[request]["message"] is not None - data[request]["code"] == 200 +def assert_ok(output: dict, code=200) -> None: + if output["success"] is False: + # convenience for debugging, this should display error + # if message is empty, consider adding helpful messages + raise ValueError(output["code"], output["message"]) + assert output["success"] is True + assert output["message"] is not None + assert output["code"] == code -def assert_errorcode(response, request, code): - data = assert_data(response) - data[request]["success"] is False - data[request]["message"] is not None - data[request]["code"] == code +def assert_errorcode(output: dict, code) -> None: + assert output["success"] is False + assert output["message"] is not None + assert output["code"] == code def assert_empty(response): @@ -23,9 +25,15 @@ def assert_empty(response): assert response.json().get("data") is None -def assert_data(response): +def get_data(response): assert response.status_code == 200 - data = response.json().get("data") + response = response.json() + + if ( + "errors" in response.keys() + ): # convenience for debugging, this will display error + raise ValueError(response["errors"]) + data = response.get("data") assert data is not None return data @@ -48,7 +56,7 @@ def request_devices(client): def graphql_get_devices(client): response = request_devices(client) - data = assert_data(response) + data = get_data(response) devices = data["api"]["devices"] assert devices is not None return devices diff --git a/tests/test_graphql/test_api_backup.py b/tests/test_graphql/test_api_backup.py index b44fd44..18d5d15 100644 --- a/tests/test_graphql/test_api_backup.py +++ b/tests/test_graphql/test_api_backup.py @@ -1,5 +1,5 @@ from os import path -from tests.test_graphql.test_backup import dummy_service, backups, raw_dummy_service +from tests.test_backup import backups from tests.common import generate_backup_query @@ -279,7 +279,7 @@ def get_data(response): if ( "errors" in response.keys() ): # convenience for debugging, this will display error - assert response["errors"] == [] + raise ValueError(response["errors"]) assert response["data"] is not None data = response["data"] return data @@ -301,7 +301,7 @@ def test_dummy_service_convertible_to_gql(dummy_service): assert gql_service is not None -def test_snapshots_empty(authorized_client, dummy_service): +def test_snapshots_empty(authorized_client, dummy_service, backups): snaps = api_snapshots(authorized_client) assert snaps == [] diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index b24bc7f..ef77414 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -8,8 +8,8 @@ from tests.common import ( generate_api_query, ) from tests.conftest import DEVICE_WE_AUTH_TESTS_WITH, TOKENS_FILE_CONTENTS -from tests.test_graphql.api_common import ( - assert_data, +from tests.test_graphql.common import ( + get_data, assert_empty, assert_ok, assert_errorcode, @@ -36,7 +36,7 @@ def graphql_get_new_device_key(authorized_client) -> str: "/graphql", json={"query": NEW_DEVICE_KEY_MUTATION}, ) - assert_ok(response, "getNewDeviceApiKey") + assert_ok(get_data(response)["api"]["getNewDeviceApiKey"]) key = response.json()["data"]["api"]["getNewDeviceApiKey"]["key"] assert key.split(" ").__len__() == 12 @@ -60,9 +60,10 @@ def graphql_try_auth_new_device(client, mnemonic_key, device_name): def graphql_authorize_new_device(client, mnemonic_key, device_name) -> str: response = graphql_try_auth_new_device(client, mnemonic_key, "new_device") - assert_ok(response, "authorizeWithNewDeviceApiKey") + assert_ok(get_data(response)["api"]["authorizeWithNewDeviceApiKey"]) token = response.json()["data"]["api"]["authorizeWithNewDeviceApiKey"]["token"] assert_token_valid(client, token) + return token def test_graphql_tokens_info(authorized_client, tokens_file): @@ -114,7 +115,7 @@ def test_graphql_delete_token(authorized_client, tokens_file): }, }, ) - assert_ok(response, "deleteDeviceApiToken") + assert_ok(get_data(response)["api"]["deleteDeviceApiToken"]) devices = graphql_get_devices(authorized_client) assert_same(devices, test_devices) @@ -130,7 +131,7 @@ def test_graphql_delete_self_token(authorized_client, tokens_file): }, }, ) - assert_errorcode(response, "deleteDeviceApiToken", 400) + assert_errorcode(get_data(response)["api"]["deleteDeviceApiToken"], 400) assert_original(authorized_client) @@ -147,7 +148,7 @@ def test_graphql_delete_nonexistent_token( }, }, ) - assert_errorcode(response, "deleteDeviceApiToken", 404) + assert_errorcode(get_data(response)["api"]["deleteDeviceApiToken"], 404) assert_original(authorized_client) @@ -180,7 +181,7 @@ def test_graphql_refresh_token(authorized_client, client, tokens_file): "/graphql", json={"query": REFRESH_TOKEN_MUTATION}, ) - assert_ok(response, "refreshDeviceApiToken") + assert_ok(get_data(response)["api"]["refreshDeviceApiToken"]) new_token = response.json()["data"]["api"]["refreshDeviceApiToken"]["token"] assert_token_valid(client, new_token) @@ -250,10 +251,10 @@ def test_graphql_get_and_delete_new_device_key(client, authorized_client, tokens "/graphql", json={"query": INVALIDATE_NEW_DEVICE_KEY_MUTATION}, ) - assert_ok(response, "invalidateNewDeviceApiKey") + assert_ok(get_data(response)["api"]["invalidateNewDeviceApiKey"]) response = graphql_try_auth_new_device(client, mnemonic_key, "new_device") - assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) + assert_errorcode(get_data(response)["api"]["authorizeWithNewDeviceApiKey"], 404) AUTHORIZE_WITH_NEW_DEVICE_KEY_MUTATION = """ @@ -285,7 +286,7 @@ def test_graphql_authorize_new_device_with_invalid_key( client, authorized_client, tokens_file ): response = graphql_try_auth_new_device(client, "invalid_token", "new_device") - assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) + assert_errorcode(get_data(response)["api"]["authorizeWithNewDeviceApiKey"], 404) assert_original(authorized_client) @@ -297,7 +298,7 @@ def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_fi devices = graphql_get_devices(authorized_client) response = graphql_try_auth_new_device(client, mnemonic_key, "new_device2") - assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) + assert_errorcode(get_data(response)["api"]["authorizeWithNewDeviceApiKey"], 404) assert graphql_get_devices(authorized_client) == devices @@ -309,7 +310,7 @@ def test_graphql_get_and_authorize_key_after_12_minutes( mock = mocker.patch(DEVICE_KEY_VALIDATION_DATETIME, NearFuture) response = graphql_try_auth_new_device(client, mnemonic_key, "new_device") - assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) + assert_errorcode(get_data(response)["api"]["authorizeWithNewDeviceApiKey"], 404) def test_graphql_authorize_without_token( diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 629bac0..f53394f 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -18,9 +18,9 @@ from tests.common import five_minutes_into_future_naive_utc as five_minutes_into from tests.common import five_minutes_into_future as five_minutes_into_future_tz from tests.common import five_minutes_into_past_naive_utc as five_minutes_into_past -from tests.test_graphql.api_common import ( +from tests.test_graphql.common import ( assert_empty, - assert_data, + get_data, assert_ok, assert_errorcode, assert_token_valid, @@ -49,9 +49,9 @@ def request_recovery_status(client): def graphql_recovery_status(client): response = request_recovery_status(client) - data = assert_data(response) + data = get_data(response) - status = data["recoveryKey"] + status = data["api"]["recoveryKey"] assert status is not None return status @@ -74,8 +74,10 @@ def request_make_new_recovery_key(client, expires_at=None, uses=None): def graphql_make_new_recovery_key(client, expires_at=None, uses=None): response = request_make_new_recovery_key(client, expires_at, uses) - assert_ok(response, "getNewRecoveryApiKey") - key = response.json()["data"]["api"]["getNewRecoveryApiKey"]["key"] + output = get_data(response)["api"]["getNewRecoveryApiKey"] + assert_ok(output) + + key = output["key"] assert key is not None assert key.split(" ").__len__() == 18 return key @@ -98,8 +100,10 @@ def request_recovery_auth(client, key, device_name): def graphql_use_recovery_key(client, key, device_name): response = request_recovery_auth(client, key, device_name) - assert_ok(response, "useRecoveryApiKey") - token = response.json()["data"]["api"]["useRecoveryApiKey"]["token"] + output = get_data(response)["api"]["useRecoveryApiKey"] + assert_ok(output) + + token = output["token"] assert token is not None assert_token_valid(client, token) set_client_token(client, token) @@ -198,8 +202,10 @@ def test_graphql_use_recovery_key_after_expiration( mock = mocker.patch(RECOVERY_KEY_VALIDATION_DATETIME, NearFuture) response = request_recovery_auth(client, key, "new_test_token3") - assert_errorcode(response, "useRecoveryApiKey", 404) - assert response.json()["data"]["api"]["useRecoveryApiKey"]["token"] is None + output = get_data(response)["api"]["useRecoveryApiKey"] + assert_errorcode(output, 404) + + assert output["token"] is None assert_original(authorized_client) status = graphql_recovery_status(authorized_client) @@ -222,8 +228,10 @@ def test_graphql_generate_recovery_key_with_expiration_in_the_past( authorized_client, expires_at=expiration_date ) - assert_errorcode(response, "getNewRecoveryApiKey", 400) - assert response.json()["data"]["api"]["getNewRecoveryApiKey"]["key"] is None + output = get_data(response)["api"]["getNewRecoveryApiKey"] + assert_errorcode(output, 400) + + assert output["key"] is None assert graphql_recovery_status(authorized_client)["exists"] is False @@ -280,7 +288,8 @@ def test_graphql_generate_recovery_key_with_limited_uses( assert status["usesLeft"] == 0 response = request_recovery_auth(client, mnemonic_key, "new_test_token3") - assert_errorcode(response, "useRecoveryApiKey", 404) + output = get_data(response)["api"]["useRecoveryApiKey"] + assert_errorcode(output, 404) def test_graphql_generate_recovery_key_with_negative_uses( @@ -288,13 +297,16 @@ def test_graphql_generate_recovery_key_with_negative_uses( ): response = request_make_new_recovery_key(authorized_client, uses=-1) - assert_errorcode(response, "getNewRecoveryApiKey", 400) - assert response.json()["data"]["api"]["getNewRecoveryApiKey"]["key"] is None + output = get_data(response)["api"]["getNewRecoveryApiKey"] + assert_errorcode(output, 400) + assert output["key"] is None + assert graphql_recovery_status(authorized_client)["exists"] is False def test_graphql_generate_recovery_key_with_zero_uses(authorized_client, tokens_file): response = request_make_new_recovery_key(authorized_client, uses=0) - assert_errorcode(response, "getNewRecoveryApiKey", 400) - assert response.json()["data"]["api"]["getNewRecoveryApiKey"]["key"] is None + output = get_data(response)["api"]["getNewRecoveryApiKey"] + assert_errorcode(output, 400) + assert output["key"] is None assert graphql_recovery_status(authorized_client)["exists"] is False diff --git a/tests/test_graphql/test_services.py b/tests/test_graphql/test_services.py new file mode 100644 index 0000000..1c1374a --- /dev/null +++ b/tests/test_graphql/test_services.py @@ -0,0 +1,517 @@ +import pytest +from typing import Generator + +from selfprivacy_api.utils.block_devices import BlockDevices + +import selfprivacy_api.services as service_module +from selfprivacy_api.services import get_service_by_id +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 + + +@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) + + +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 +} +""" + +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 + + +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, only_dummy_service): + dummy_service = only_dummy_service + dummy_service.set_movable(False) + mutation_response = api_move(authorized_client, dummy_service, "sda1") + 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 + 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) + + # is there a meaning in returning the service in this? + assert data["service"] is not 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 + assert data["job"] is not None + + +def test_mailservice_cannot_enable_disable(authorized_client): + mailservice = get_service_by_id("email") + + 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 diff --git a/tests/test_graphql/test_ssh.py b/tests/test_graphql/test_ssh.py index eabf049..2a2c259 100644 --- a/tests/test_graphql/test_ssh.py +++ b/tests/test_graphql/test_ssh.py @@ -1,9 +1,25 @@ # pylint: disable=redefined-outer-name # pylint: disable=unused-argument import pytest +from typing import Optional -from tests.common import read_json -from tests.test_graphql.common import assert_empty +from selfprivacy_api.graphql.mutations.system_mutations import SystemMutations +from selfprivacy_api.graphql.queries.system import System + +# only allowed in fixtures and utils +from selfprivacy_api.actions.ssh import remove_ssh_key, get_ssh_settings +from selfprivacy_api.actions.users import get_users, UserDataUserOrigin + +from tests.common import read_json, generate_system_query, generate_users_query +from tests.test_graphql.common import ( + assert_empty, + assert_ok, + get_data, + assert_errorcode, +) +from tests.test_graphql.test_users import API_USERS_INFO + +key_users = ["root", "tester", "user1", "user2", "user3"] class ProcessMock: @@ -13,7 +29,7 @@ class ProcessMock: self.args = args self.kwargs = kwargs - def communicate(): # pylint: disable=no-method-argument + def communicate(self): # pylint: disable=no-method-argument return (b"NEW_HASHED", None) returncode = 0 @@ -40,7 +56,56 @@ def some_users(mocker, datadir): return datadir -# TESTS ######################################################## +@pytest.fixture +def no_rootkeys(generic_userdata): + for rootkey in get_ssh_settings().rootKeys: + remove_ssh_key("root", rootkey) + assert get_ssh_settings().rootKeys == [] + + +@pytest.fixture +def no_keys(generic_userdata): + # this removes root and admin keys too + + users = get_users() + for user in users: + for key in user.ssh_keys: + remove_ssh_key(user.username, key) + users = get_users() + for user in users: + assert user.ssh_keys == [] + + +@pytest.fixture +def no_admin_key(generic_userdata, authorized_client): + admin_keys = api_get_user_keys(authorized_client, admin_name()) + + for admin_key in admin_keys: + remove_ssh_key(admin_name(), admin_key) + + assert api_get_user_keys(authorized_client, admin_name()) == [] + + +def admin_name() -> Optional[str]: + users = get_users() + for user in users: + if user.origin == UserDataUserOrigin.PRIMARY: + return user.username + return None + + +def api_get_user_keys(authorized_client, user: str): + response = authorized_client.post( + "/graphql", + json={ + "query": generate_users_query([API_USERS_INFO]), + }, + ) + data = get_data(response)["users"]["allUsers"] + for _user in data: + if _user["username"] == user: + return _user["sshKeys"] + return None API_CREATE_SSH_KEY_MUTATION = """ @@ -59,6 +124,250 @@ mutation addSshKey($sshInput: SshMutationInput!) { } """ +API_SET_SSH_SETTINGS = """ +mutation enableSsh($settings: SSHSettingsInput!) { + system { + changeSshSettings(settings: $settings) { + success + message + code + enable + passwordAuthentication + } + } +} + +""" + +API_SSH_SETTINGS_QUERY = """ +settings { + ssh { + enable + passwordAuthentication + } +} +""" + + +API_ROOTKEYS_QUERY = """ +settings { + ssh { + rootSshKeys + } +} +""" + + +def api_ssh_settings_raw(client): + return client.post( + "/graphql", + json={"query": generate_system_query([API_SSH_SETTINGS_QUERY])}, + ) + + +def api_rootkeys_raw(client): + return client.post( + "/graphql", + json={"query": generate_system_query([API_ROOTKEYS_QUERY])}, + ) + + +def api_add_ssh_key(authorized_client, user: str, key: str): + response = authorized_client.post( + "/graphql", + json={ + "query": API_CREATE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": user, + "sshKey": key, + }, + }, + }, + ) + data = get_data(response) + result = data["users"]["addSshKey"] + assert result is not None + return result + + +def api_remove_ssh_key(authorized_client, user: str, key: str): + response = authorized_client.post( + "/graphql", + json={ + "query": API_REMOVE_SSH_KEY_MUTATION, + "variables": { + "sshInput": { + "username": user, + "sshKey": key, + }, + }, + }, + ) + data = get_data(response) + result = data["users"]["removeSshKey"] + assert result is not None + return result + + +def api_rootkeys(authorized_client): + response = api_rootkeys_raw(authorized_client) + data = get_data(response) + result = data["system"]["settings"]["ssh"]["rootSshKeys"] + assert result is not None + return result + + +def api_ssh_settings(authorized_client): + response = api_ssh_settings_raw(authorized_client) + data = get_data(response) + result = data["system"]["settings"]["ssh"] + assert result is not None + return result + + +def api_set_ssh_settings_dict(authorized_client, dict): + response = authorized_client.post( + "/graphql", + json={ + "query": API_SET_SSH_SETTINGS, + "variables": { + "settings": dict, + }, + }, + ) + data = get_data(response) + result = data["system"]["changeSshSettings"] + assert result is not None + return result + + +def api_set_ssh_settings(authorized_client, enable: bool, password_auth: bool): + return api_set_ssh_settings_dict( + authorized_client, + { + "enable": enable, + "passwordAuthentication": password_auth, + }, + ) + + +# TESTS ######################################################## + + +def test_graphql_ssh_query(authorized_client, some_users): + settings = api_ssh_settings(authorized_client) + assert settings["enable"] is True + assert settings["passwordAuthentication"] is True + + +def test_graphql_get_ssh_settings_unauthorized(client, some_users): + response = api_ssh_settings_raw(client) + assert_empty(response) + + +def test_graphql_change_ssh_settings_unauthorized(client, some_users): + response = client.post( + "/graphql", + json={ + "query": API_SET_SSH_SETTINGS, + "variables": { + "sshInput": { + "enable": True, + "passwordAuthentication": True, + }, + }, + }, + ) + assert_empty(response) + + +def assert_includes(smaller_dict: dict, bigger_dict: dict): + for item in smaller_dict.items(): + assert item in bigger_dict.items() + + +available_settings = [ + {"enable": True, "passwordAuthentication": True}, + {"enable": True, "passwordAuthentication": False}, + {"enable": False, "passwordAuthentication": True}, + {"enable": False, "passwordAuthentication": False}, +] + + +original_settings = [ + {"enable": True, "passwordAuthentication": True}, + {"enable": True, "passwordAuthentication": False}, + {"enable": False, "passwordAuthentication": True}, + {"enable": False, "passwordAuthentication": False}, +] + + +@pytest.mark.parametrize("original_settings", original_settings) +@pytest.mark.parametrize("settings", available_settings) +def test_graphql_readwrite_ssh_settings( + authorized_client, some_users, settings, original_settings +): + + # Userdata-related tests like undefined fields are in actions-level tests. + output = api_set_ssh_settings_dict(authorized_client, original_settings) + assert_includes(api_ssh_settings(authorized_client), output) + + output = api_set_ssh_settings_dict(authorized_client, settings) + assert_ok(output) + assert_includes(settings, output) + if "enable" not in settings.keys(): + assert output["enable"] == original_settings["enable"] + assert_includes(api_ssh_settings(authorized_client), output) + + +forbidden_settings = [ + # we include this here so that if the next version makes the fields + # optional, the tests will remind the person that tests are to be extended accordingly + {"enable": True}, + {"passwordAuthentication": True}, +] + + +@pytest.mark.parametrize("original_settings", original_settings) +@pytest.mark.parametrize("settings", forbidden_settings) +def test_graphql_readwrite_ssh_settings_partial( + authorized_client, some_users, settings, original_settings +): + + output = api_set_ssh_settings_dict(authorized_client, original_settings) + with pytest.raises(Exception): + output = api_set_ssh_settings_dict(authorized_client, settings) + + +def test_graphql_disable_twice(authorized_client, some_users): + output = api_set_ssh_settings(authorized_client, enable=False, password_auth=False) + assert_ok(output) + assert output["enable"] is False + assert output["passwordAuthentication"] is False + + output = api_set_ssh_settings(authorized_client, enable=False, password_auth=False) + assert_ok(output) + assert output["enable"] is False + assert output["passwordAuthentication"] is False + + +def test_graphql_enable_twice(authorized_client, some_users): + output = api_set_ssh_settings(authorized_client, enable=True, password_auth=True) + assert_ok(output) + assert output["enable"] is True + assert output["passwordAuthentication"] is True + assert_includes(api_ssh_settings(authorized_client), output) + + output = api_set_ssh_settings(authorized_client, enable=True, password_auth=True) + assert_ok(output) + assert output["enable"] is True + assert output["passwordAuthentication"] is True + assert_includes(api_ssh_settings(authorized_client), output) + + +############## KEYS + def test_graphql_add_ssh_key_unauthorized(client, some_users, mock_subprocess_popen): response = client.post( @@ -76,106 +385,81 @@ def test_graphql_add_ssh_key_unauthorized(client, some_users, mock_subprocess_po assert_empty(response) -def test_graphql_add_ssh_key(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_SSH_KEY_MUTATION, - "variables": { - "sshInput": { - "username": "user1", - "sshKey": "ssh-rsa KEY test_key@pc", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None +# Unauth getting of keys is tested in test_users.py because it is a part of users interface - assert response.json()["data"]["users"]["addSshKey"]["code"] == 201 - assert response.json()["data"]["users"]["addSshKey"]["message"] is not None - assert response.json()["data"]["users"]["addSshKey"]["success"] is True - assert response.json()["data"]["users"]["addSshKey"]["user"]["username"] == "user1" - assert response.json()["data"]["users"]["addSshKey"]["user"]["sshKeys"] == [ - "ssh-rsa KEY user1@pc", +def test_graphql_get_root_key(authorized_client, some_users): + assert api_rootkeys(authorized_client) == ["ssh-ed25519 KEY test@pc"] + + +def test_graphql_get_root_key_when_none(authorized_client, no_rootkeys): + assert api_rootkeys(authorized_client) == [] + + +# Getting admin keys when they are present is tested in test_users.py + + +def test_get_admin_key_when_none(authorized_client, no_admin_key): + assert api_get_user_keys(authorized_client, admin_name()) == [] + + +@pytest.mark.parametrize("user", key_users) +def test_graphql_add_ssh_key_when_none(authorized_client, no_keys, user): + key1 = "ssh-rsa KEY test_key@pc" + if user == "root": + assert api_rootkeys(authorized_client) == [] + else: + assert api_get_user_keys(authorized_client, user) == [] + + output = api_add_ssh_key(authorized_client, user, key1) + + assert_ok(output, code=201) + + assert output["user"]["username"] == user + assert output["user"]["sshKeys"] == [key1] + + if user == "root": + assert api_rootkeys(authorized_client) == [key1] + else: + assert api_get_user_keys(authorized_client, user) == [key1] + + +@pytest.mark.parametrize("user", key_users) +def test_graphql_add_ssh_key_one_more(authorized_client, no_keys, user): + keys = [ "ssh-rsa KEY test_key@pc", + "ssh-rsa KEY2 test_key@pc", ] + output = api_add_ssh_key(authorized_client, user, keys[0]) + assert output["user"]["sshKeys"] == [keys[0]] + + output = api_add_ssh_key(authorized_client, user, keys[1]) + + assert_ok(output, code=201) + + assert output["user"]["username"] == user + assert output["user"]["sshKeys"] == keys + + if user == "root": + assert api_rootkeys(authorized_client) == keys + else: + assert api_get_user_keys(authorized_client, user) == keys -def test_graphql_add_root_ssh_key(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_SSH_KEY_MUTATION, - "variables": { - "sshInput": { - "username": "root", - "sshKey": "ssh-rsa KEY test_key@pc", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None +@pytest.mark.parametrize("user", key_users) +def test_graphql_add_ssh_key_same(authorized_client, no_keys, user): + key = "ssh-rsa KEY test_key@pc" + output = api_add_ssh_key(authorized_client, user, key) + assert output["user"]["sshKeys"] == [key] - assert response.json()["data"]["users"]["addSshKey"]["code"] == 201 - assert response.json()["data"]["users"]["addSshKey"]["message"] is not None - assert response.json()["data"]["users"]["addSshKey"]["success"] is True - - assert response.json()["data"]["users"]["addSshKey"]["user"]["username"] == "root" - assert response.json()["data"]["users"]["addSshKey"]["user"]["sshKeys"] == [ - "ssh-ed25519 KEY test@pc", - "ssh-rsa KEY test_key@pc", - ] + output = api_add_ssh_key(authorized_client, user, key) + assert_errorcode(output, 409) -def test_graphql_add_main_ssh_key(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_SSH_KEY_MUTATION, - "variables": { - "sshInput": { - "username": "tester", - "sshKey": "ssh-rsa KEY test_key@pc", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - - assert response.json()["data"]["users"]["addSshKey"]["code"] == 201 - assert response.json()["data"]["users"]["addSshKey"]["message"] is not None - assert response.json()["data"]["users"]["addSshKey"]["success"] is True - - assert response.json()["data"]["users"]["addSshKey"]["user"]["username"] == "tester" - assert response.json()["data"]["users"]["addSshKey"]["user"]["sshKeys"] == [ - "ssh-rsa KEY test@pc", - "ssh-rsa KEY test_key@pc", - ] - - -def test_graphql_add_bad_ssh_key(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_SSH_KEY_MUTATION, - "variables": { - "sshInput": { - "username": "user1", - "sshKey": "trust me, this is the ssh key", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - - assert response.json()["data"]["users"]["addSshKey"]["code"] == 400 - assert response.json()["data"]["users"]["addSshKey"]["message"] is not None - assert response.json()["data"]["users"]["addSshKey"]["success"] is False +@pytest.mark.parametrize("user", key_users) +def test_graphql_add_bad_ssh_key(authorized_client, some_users, user): + output = api_add_ssh_key(authorized_client, user, "trust me, this is the ssh key") + assert_errorcode(output, 400) def test_graphql_add_ssh_key_nonexistent_user( @@ -234,129 +518,35 @@ def test_graphql_remove_ssh_key_unauthorized(client, some_users, mock_subprocess assert_empty(response) -def test_graphql_remove_ssh_key(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_REMOVE_SSH_KEY_MUTATION, - "variables": { - "sshInput": { - "username": "user1", - "sshKey": "ssh-rsa KEY user1@pc", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None +@pytest.mark.parametrize("user", key_users) +def test_graphql_remove_ssh_key(authorized_client, no_keys, user): + keys = [ + "ssh-rsa KEY test_key@pc", + "ssh-rsa KEY2 test_key@pc", + ] + output = api_add_ssh_key(authorized_client, user, keys[0]) + output = api_add_ssh_key(authorized_client, user, keys[1]) + assert output["user"]["sshKeys"] == keys - assert response.json()["data"]["users"]["removeSshKey"]["code"] == 200 - assert response.json()["data"]["users"]["removeSshKey"]["message"] is not None - assert response.json()["data"]["users"]["removeSshKey"]["success"] is True + output = api_remove_ssh_key(authorized_client, user, keys[1]) + assert_ok(output) + assert output["user"]["username"] == user + assert output["user"]["sshKeys"] == [keys[0]] - assert ( - response.json()["data"]["users"]["removeSshKey"]["user"]["username"] == "user1" - ) - assert response.json()["data"]["users"]["removeSshKey"]["user"]["sshKeys"] == [] + if user == "root": + assert api_rootkeys(authorized_client) == [keys[0]] + else: + assert api_get_user_keys(authorized_client, user) == [keys[0]] -def test_graphql_remove_root_ssh_key( - authorized_client, some_users, mock_subprocess_popen -): - response = authorized_client.post( - "/graphql", - json={ - "query": API_REMOVE_SSH_KEY_MUTATION, - "variables": { - "sshInput": { - "username": "root", - "sshKey": "ssh-ed25519 KEY test@pc", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - - assert response.json()["data"]["users"]["removeSshKey"]["code"] == 200 - assert response.json()["data"]["users"]["removeSshKey"]["message"] is not None - assert response.json()["data"]["users"]["removeSshKey"]["success"] is True - - assert ( - response.json()["data"]["users"]["removeSshKey"]["user"]["username"] == "root" - ) - assert response.json()["data"]["users"]["removeSshKey"]["user"]["sshKeys"] == [] - - -def test_graphql_remove_main_ssh_key( - authorized_client, some_users, mock_subprocess_popen -): - response = authorized_client.post( - "/graphql", - json={ - "query": API_REMOVE_SSH_KEY_MUTATION, - "variables": { - "sshInput": { - "username": "tester", - "sshKey": "ssh-rsa KEY test@pc", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - - assert response.json()["data"]["users"]["removeSshKey"]["code"] == 200 - assert response.json()["data"]["users"]["removeSshKey"]["message"] is not None - assert response.json()["data"]["users"]["removeSshKey"]["success"] is True - - assert ( - response.json()["data"]["users"]["removeSshKey"]["user"]["username"] == "tester" - ) - assert response.json()["data"]["users"]["removeSshKey"]["user"]["sshKeys"] == [] - - -def test_graphql_remove_nonexistent_ssh_key( - authorized_client, some_users, mock_subprocess_popen -): - response = authorized_client.post( - "/graphql", - json={ - "query": API_REMOVE_SSH_KEY_MUTATION, - "variables": { - "sshInput": { - "username": "user1", - "sshKey": "ssh-rsa KEY test_key@pc", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - - assert response.json()["data"]["users"]["removeSshKey"]["code"] == 404 - assert response.json()["data"]["users"]["removeSshKey"]["message"] is not None - assert response.json()["data"]["users"]["removeSshKey"]["success"] is False +@pytest.mark.parametrize("user", key_users) +def test_graphql_remove_nonexistent_ssh_key(authorized_client, some_users, user): + output = api_remove_ssh_key(authorized_client, user, "ssh-rsa nonexistent") + assert_errorcode(output, 404) def test_graphql_remove_ssh_key_nonexistent_user( authorized_client, some_users, mock_subprocess_popen ): - response = authorized_client.post( - "/graphql", - json={ - "query": API_REMOVE_SSH_KEY_MUTATION, - "variables": { - "sshInput": { - "username": "user666", - "sshKey": "ssh-rsa KEY test_key@pc", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - - assert response.json()["data"]["users"]["removeSshKey"]["code"] == 404 - assert response.json()["data"]["users"]["removeSshKey"]["message"] is not None - assert response.json()["data"]["users"]["removeSshKey"]["success"] is False + output = api_remove_ssh_key(authorized_client, "user666", "ssh-rsa KEY test_key@pc") + assert_errorcode(output, 404) diff --git a/tests/test_graphql/test_system.py b/tests/test_graphql/test_system.py index ed00268..c318fe7 100644 --- a/tests/test_graphql/test_system.py +++ b/tests/test_graphql/test_system.py @@ -6,6 +6,7 @@ import pytest from tests.common import generate_system_query, read_json from tests.test_graphql.common import assert_empty +from tests.test_dkim import no_dkim_file, dkim_file @pytest.fixture @@ -332,6 +333,29 @@ def test_graphql_get_domain( ) +def test_graphql_get_domain_no_dkim( + authorized_client, + domain_file, + mock_get_ip4, + mock_get_ip6, + no_dkim_file, + turned_on, +): + """Test no DKIM file situation gets properly handled""" + response = authorized_client.post( + "/graphql", + json={ + "query": generate_system_query([API_GET_DOMAIN_INFO]), + }, + ) + assert response.status_code == 200 + assert response.json().get("data") is not None + dns_records = response.json()["data"]["system"]["domainInfo"]["requiredDnsRecords"] + for record in dns_records: + if record["name"] == "selector._domainkey": + raise ValueError("unexpected record found:", record) + + API_GET_TIMEZONE = """ settings { timezone diff --git a/tests/test_graphql/test_system/turned_on.json b/tests/test_graphql/test_system/turned_on.json index c6b758b..240c6c9 100644 --- a/tests/test_graphql/test_system/turned_on.json +++ b/tests/test_graphql/test_system/turned_on.json @@ -3,18 +3,10 @@ "token": "TEST_TOKEN", "enableSwagger": false }, - "bitwarden": { - "enable": true - }, "databasePassword": "PASSWORD", "domain": "test.tld", "hashedMasterPassword": "HASHED_PASSWORD", "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, "resticPassword": "PASS", "ssh": { "enable": true, @@ -24,17 +16,27 @@ ] }, "username": "tester", - "gitea": { - "enable": true - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "jitsi": { - "enable": true + "modules": { + "gitea": { + "enable": true + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "jitsi": { + "enable": true + }, + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "bitwarden": { + "enable": true + } }, "autoUpgrade": { "enable": true, diff --git a/tests/test_graphql/test_system_nixos_tasks.py b/tests/test_graphql/test_system_nixos_tasks.py index b292fda..6052e9f 100644 --- a/tests/test_graphql/test_system_nixos_tasks.py +++ b/tests/test_graphql/test_system_nixos_tasks.py @@ -23,15 +23,6 @@ class ProcessMock: returncode = 0 -class BrokenServiceMock(ProcessMock): - """Mock subprocess.Popen for broken service""" - - def communicate(): # pylint: disable=no-method-argument - return (b"Testing error", None) - - returncode = 3 - - @pytest.fixture def mock_subprocess_popen(mocker): mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock) diff --git a/tests/test_graphql/test_users.py b/tests/test_graphql/test_users.py index af40981..99f5934 100644 --- a/tests/test_graphql/test_users.py +++ b/tests/test_graphql/test_users.py @@ -6,7 +6,13 @@ from tests.common import ( generate_users_query, read_json, ) -from tests.test_graphql.common import assert_empty +from selfprivacy_api.utils import WriteUserData +from tests.test_graphql.common import ( + assert_empty, + assert_errorcode, + assert_ok, + get_data, +) invalid_usernames = [ "messagebus", @@ -89,6 +95,15 @@ def undefined_settings(mocker, datadir): return datadir +@pytest.fixture +def no_users_no_admin_nobody(undefined_settings): + datadir = undefined_settings + with WriteUserData() as data: + del data["username"] + del data["sshKeys"] + return datadir + + class ProcessMock: """Mock subprocess.Popen""" @@ -118,6 +133,17 @@ allUsers { """ +def api_all_users(authorized_client): + response = authorized_client.post( + "/graphql", + json={ + "query": generate_users_query([API_USERS_INFO]), + }, + ) + output = get_data(response)["users"]["allUsers"] + return output + + def test_graphql_get_users_unauthorized(client, some_users, mock_subprocess_popen): """Test wrong auth""" response = client.post( @@ -170,6 +196,38 @@ def test_graphql_get_no_users(authorized_client, no_users, mock_subprocess_popen ] +def test_graphql_get_users_undefined_but_admin(authorized_client, undefined_settings): + response = authorized_client.post( + "/graphql", + json={ + "query": generate_users_query([API_USERS_INFO]), + }, + ) + assert response.status_code == 200 + assert response.json().get("data") is not None + + assert len(response.json()["data"]["users"]["allUsers"]) == 1 + assert response.json()["data"]["users"]["allUsers"][0]["username"] == "tester" + assert response.json()["data"]["users"]["allUsers"][0]["sshKeys"] == [ + "ssh-rsa KEY test@pc" + ] + + +def test_graphql_get_users_undefined_no_admin( + authorized_client, no_users_no_admin_nobody +): + response = authorized_client.post( + "/graphql", + json={ + "query": generate_users_query([API_USERS_INFO]), + }, + ) + assert response.status_code == 200 + assert response.json().get("data") is not None + + assert len(response.json()["data"]["users"]["allUsers"]) == 0 + + API_GET_USERS = """ query TestUsers($username: String!) { users { @@ -216,6 +274,23 @@ def test_graphql_get_one_user(authorized_client, one_user, mock_subprocess_popen ] +def test_graphql_get_some_user_undefined(authorized_client, undefined_settings): + + response = authorized_client.post( + "/graphql", + json={ + "query": API_GET_USERS, + "variables": { + "username": "user1", + }, + }, + ) + assert response.status_code == 200 + assert response.json().get("data") is not None + + assert response.json()["data"]["users"]["getUser"] is None + + def test_graphql_get_some_user(authorized_client, some_users, mock_subprocess_popen): response = authorized_client.post( "/graphql", @@ -309,248 +384,135 @@ mutation createUser($user: UserMutationInput!) { """ -def test_graphql_add_user_unauthorize(client, one_user, mock_subprocess_popen): - response = client.post( +def api_add_user_json(authorized_client, user_json: dict): + # lowlevel for deeper testing of edgecases + return authorized_client.post( "/graphql", json={ "query": API_CREATE_USERS_MUTATION, "variables": { - "user": { - "username": "user2", - "password": "12345678", - }, + "user": user_json, }, }, ) + + +def api_add_user(authorized_client, username, password): + response = api_add_user_json( + authorized_client, {"username": username, "password": password} + ) + output = get_data(response)["users"]["createUser"] + return output + + +def test_graphql_add_user_unauthorized(client, one_user, mock_subprocess_popen): + response = api_add_user_json(client, {"username": "user2", "password": "12345678"}) assert_empty(response) def test_graphql_add_user(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_USERS_MUTATION, - "variables": { - "user": { - "username": "user2", - "password": "12345678", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None + output = api_add_user(authorized_client, "user2", password="12345678") + assert_ok(output, code=201) - assert response.json()["data"]["users"]["createUser"]["message"] is not None - assert response.json()["data"]["users"]["createUser"]["code"] == 201 - assert response.json()["data"]["users"]["createUser"]["success"] is True - - assert response.json()["data"]["users"]["createUser"]["user"]["username"] == "user2" - assert response.json()["data"]["users"]["createUser"]["user"]["sshKeys"] == [] + assert output["user"]["username"] == "user2" + assert output["user"]["sshKeys"] == [] -def test_graphql_add_undefined_settings( +def test_graphql_add_user_when_undefined_settings( authorized_client, undefined_settings, mock_subprocess_popen ): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_USERS_MUTATION, - "variables": { - "user": { - "username": "user2", - "password": "12345678", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None + output = api_add_user(authorized_client, "user2", password="12345678") + assert_ok(output, code=201) - assert response.json()["data"]["users"]["createUser"]["message"] is not None - assert response.json()["data"]["users"]["createUser"]["code"] == 201 - assert response.json()["data"]["users"]["createUser"]["success"] is True - - assert response.json()["data"]["users"]["createUser"]["user"]["username"] == "user2" - assert response.json()["data"]["users"]["createUser"]["user"]["sshKeys"] == [] + assert output["user"]["username"] == "user2" + assert output["user"]["sshKeys"] == [] -def test_graphql_add_without_password( - authorized_client, one_user, mock_subprocess_popen -): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_USERS_MUTATION, - "variables": { - "user": { - "username": "user2", - "password": "", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - - assert response.json()["data"]["users"]["createUser"]["message"] is not None - assert response.json()["data"]["users"]["createUser"]["code"] == 400 - assert response.json()["data"]["users"]["createUser"]["success"] is False - - assert response.json()["data"]["users"]["createUser"]["user"] is None +users_witn_empty_fields = [ + {"username": "user2", "password": ""}, + {"username": "", "password": "12345678"}, + {"username": "", "password": ""}, +] -def test_graphql_add_without_both(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_USERS_MUTATION, - "variables": { - "user": { - "username": "", - "password": "", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None +@pytest.mark.parametrize("user_json", users_witn_empty_fields) +def test_graphql_add_with_empty_fields(authorized_client, one_user, user_json): + response = api_add_user_json(authorized_client, user_json) + output = get_data(response)["users"]["createUser"] - assert response.json()["data"]["users"]["createUser"]["message"] is not None - assert response.json()["data"]["users"]["createUser"]["code"] == 400 - assert response.json()["data"]["users"]["createUser"]["success"] is False + assert_errorcode(output, 400) + assert output["user"] is None - assert response.json()["data"]["users"]["createUser"]["user"] is None + +users_witn_undefined_fields = [ + {"username": "user2"}, + {"password": "12345678"}, + {}, +] + + +@pytest.mark.parametrize("user_json", users_witn_undefined_fields) +def test_graphql_add_with_undefined_fields(authorized_client, one_user, user_json): + # checking that all fields are mandatory + response = api_add_user_json(authorized_client, user_json) + + assert response.json()["errors"] is not None + assert response.json()["errors"] != [] @pytest.mark.parametrize("username", invalid_usernames) def test_graphql_add_system_username( authorized_client, one_user, mock_subprocess_popen, username ): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_USERS_MUTATION, - "variables": { - "user": { - "username": username, - "password": "12345678", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None + output = api_add_user(authorized_client, username, password="12345678") - assert response.json()["data"]["users"]["createUser"]["message"] is not None - assert response.json()["data"]["users"]["createUser"]["code"] == 409 - assert response.json()["data"]["users"]["createUser"]["success"] is False - - assert response.json()["data"]["users"]["createUser"]["user"] is None + assert_errorcode(output, code=409) + assert output["user"] is None -def test_graphql_add_existing_user(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_USERS_MUTATION, - "variables": { - "user": { - "username": "user1", - "password": "12345678", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None +def test_graphql_add_existing_user(authorized_client, one_user): + output = api_add_user(authorized_client, "user1", password="12345678") - assert response.json()["data"]["users"]["createUser"]["message"] is not None - assert response.json()["data"]["users"]["createUser"]["code"] == 409 - assert response.json()["data"]["users"]["createUser"]["success"] is False - - assert response.json()["data"]["users"]["createUser"]["user"]["username"] == "user1" - assert ( - response.json()["data"]["users"]["createUser"]["user"]["sshKeys"][0] - == "ssh-rsa KEY user1@pc" - ) + assert_errorcode(output, code=409) + assert output["user"]["username"] == "user1" + assert output["user"]["sshKeys"][0] == "ssh-rsa KEY user1@pc" -def test_graphql_add_main_user(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_USERS_MUTATION, - "variables": { - "user": { - "username": "tester", - "password": "12345678", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None +def test_graphql_add_main_user(authorized_client, one_user): + output = api_add_user(authorized_client, "tester", password="12345678") - assert response.json()["data"]["users"]["createUser"]["message"] is not None - assert response.json()["data"]["users"]["createUser"]["code"] == 409 - assert response.json()["data"]["users"]["createUser"]["success"] is False + assert_errorcode(output, code=409) + assert output["user"]["username"] == "tester" + assert output["user"]["sshKeys"][0] == "ssh-rsa KEY test@pc" - assert ( - response.json()["data"]["users"]["createUser"]["user"]["username"] == "tester" - ) - assert ( - response.json()["data"]["users"]["createUser"]["user"]["sshKeys"][0] - == "ssh-rsa KEY test@pc" - ) + +def test_graphql_add_user_when_no_admin_defined( + authorized_client, no_users_no_admin_nobody +): + output = api_add_user(authorized_client, "tester", password="12345678") + + assert_errorcode(output, code=400) + assert output["user"] is None def test_graphql_add_long_username(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_USERS_MUTATION, - "variables": { - "user": { - "username": "a" * 32, - "password": "12345678", - }, - }, - }, - ) - assert response.json().get("data") is not None + output = api_add_user(authorized_client, "a" * 32, password="12345678") - assert response.json()["data"]["users"]["createUser"]["message"] is not None - assert response.json()["data"]["users"]["createUser"]["code"] == 400 - assert response.json()["data"]["users"]["createUser"]["success"] is False - - assert response.json()["data"]["users"]["createUser"]["user"] is None + assert_errorcode(output, code=400) + assert output["user"] is None -@pytest.mark.parametrize("username", ["", "1", "фыр", "user1@", "^-^"]) +# TODO: maybe make a username generating function to make a more comprehensive invalid username test +@pytest.mark.parametrize( + "username", ["", "1", "фыр", "user1@", "^-^", "№:%##$^&@$&^()_"] +) def test_graphql_add_invalid_username( authorized_client, one_user, mock_subprocess_popen, username ): - response = authorized_client.post( - "/graphql", - json={ - "query": API_CREATE_USERS_MUTATION, - "variables": { - "user": { - "username": username, - "password": "12345678", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None + output = api_add_user(authorized_client, username, password="12345678") - assert response.json()["data"]["users"]["createUser"]["message"] is not None - assert response.json()["data"]["users"]["createUser"]["code"] == 400 - assert response.json()["data"]["users"]["createUser"]["success"] is False - - assert response.json()["data"]["users"]["createUser"]["user"] is None + assert_errorcode(output, code=400) + assert output["user"] is None API_DELETE_USER_MUTATION = """ @@ -592,6 +554,11 @@ def test_graphql_delete_user(authorized_client, some_users, mock_subprocess_pope assert response.json()["data"]["users"]["deleteUser"]["message"] is not None assert response.json()["data"]["users"]["deleteUser"]["success"] is True + new_users = api_all_users(authorized_client) + assert len(new_users) == 3 + usernames = [user["username"] for user in new_users] + assert set(usernames) == set(["user2", "user3", "tester"]) + @pytest.mark.parametrize("username", ["", "def"]) def test_graphql_delete_nonexistent_users( diff --git a/tests/test_graphql/test_localsecret.py b/tests/test_localsecret.py similarity index 100% rename from tests/test_graphql/test_localsecret.py rename to tests/test_localsecret.py diff --git a/tests/test_migrations.py b/tests/test_migrations.py new file mode 100644 index 0000000..55f311a --- /dev/null +++ b/tests/test_migrations.py @@ -0,0 +1,60 @@ +import pytest + +from selfprivacy_api.migrations.modules_in_json import CreateModulesField +from selfprivacy_api.utils import ReadUserData, WriteUserData +from selfprivacy_api.services import get_all_services + + +@pytest.fixture() +def stray_services(mocker, datadir): + mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "strays.json") + return datadir + + +@pytest.fixture() +def empty_json(generic_userdata): + with WriteUserData() as data: + data.clear() + + with ReadUserData() as data: + assert len(data.keys()) == 0 + + return + + +def test_modules_empty_json(empty_json): + with ReadUserData() as data: + assert "modules" not in data.keys() + + assert CreateModulesField().is_migration_needed() + + CreateModulesField().migrate() + assert not CreateModulesField().is_migration_needed() + + with ReadUserData() as data: + assert "modules" in data.keys() + + +@pytest.mark.parametrize("modules_field", [True, False]) +def test_modules_stray_services(modules_field, stray_services): + if not modules_field: + with WriteUserData() as data: + del data["modules"] + assert CreateModulesField().is_migration_needed() + + CreateModulesField().migrate() + + for service in get_all_services(): + # assumes we do not tolerate previous format + assert service.is_enabled() + if service.get_id() == "email": + continue + with ReadUserData() as data: + assert service.get_id() in data["modules"].keys() + assert service.get_id() not in data.keys() + + assert not CreateModulesField().is_migration_needed() + + +def test_modules_no_migration_on_generic_data(generic_userdata): + assert not CreateModulesField().is_migration_needed() diff --git a/tests/test_migrations/strays.json b/tests/test_migrations/strays.json new file mode 100644 index 0000000..ee81350 --- /dev/null +++ b/tests/test_migrations/strays.json @@ -0,0 +1,23 @@ +{ + "bitwarden": { + "enable": true + }, + "nextcloud": { + "adminPassword": "ADMIN", + "databasePassword": "ADMIN", + "enable": true + }, + "gitea": { + "enable": true + }, + "ocserv": { + "enable": true + }, + "pleroma": { + "enable": true + }, + "jitsi": { + "enable": true + }, + "modules": {} +} diff --git a/tests/test_graphql/test_repository/test_json_tokens_repository.py b/tests/test_repository/test_json_tokens_repository.py similarity index 100% rename from tests/test_graphql/test_repository/test_json_tokens_repository.py rename to tests/test_repository/test_json_tokens_repository.py diff --git a/tests/test_repository/test_json_tokens_repository/empty_keys.json b/tests/test_repository/test_json_tokens_repository/empty_keys.json new file mode 100644 index 0000000..2131ddf --- /dev/null +++ b/tests/test_repository/test_json_tokens_repository/empty_keys.json @@ -0,0 +1,9 @@ +{ + "tokens": [ + { + "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", + "name": "primary_token", + "date": "2022-07-15 17:41:31.675698" + } + ] +} diff --git a/tests/test_graphql/test_repository/test_json_tokens_repository/null_keys.json b/tests/test_repository/test_json_tokens_repository/null_keys.json similarity index 100% rename from tests/test_graphql/test_repository/test_json_tokens_repository/null_keys.json rename to tests/test_repository/test_json_tokens_repository/null_keys.json diff --git a/tests/test_graphql/test_repository/test_json_tokens_repository/tokens.json b/tests/test_repository/test_json_tokens_repository/tokens.json similarity index 100% rename from tests/test_graphql/test_repository/test_json_tokens_repository/tokens.json rename to tests/test_repository/test_json_tokens_repository/tokens.json diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_repository/test_tokens_repository.py similarity index 100% rename from tests/test_graphql/test_repository/test_tokens_repository.py rename to tests/test_repository/test_tokens_repository.py diff --git a/tests/test_repository/test_tokens_repository/empty_keys.json b/tests/test_repository/test_tokens_repository/empty_keys.json new file mode 100644 index 0000000..2131ddf --- /dev/null +++ b/tests/test_repository/test_tokens_repository/empty_keys.json @@ -0,0 +1,9 @@ +{ + "tokens": [ + { + "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", + "name": "primary_token", + "date": "2022-07-15 17:41:31.675698" + } + ] +} diff --git a/tests/test_graphql/test_repository/test_tokens_repository/null_keys.json b/tests/test_repository/test_tokens_repository/null_keys.json similarity index 100% rename from tests/test_graphql/test_repository/test_tokens_repository/null_keys.json rename to tests/test_repository/test_tokens_repository/null_keys.json diff --git a/tests/test_graphql/test_repository/test_tokens_repository/tokens.json b/tests/test_repository/test_tokens_repository/tokens.json similarity index 100% rename from tests/test_graphql/test_repository/test_tokens_repository/tokens.json rename to tests/test_repository/test_tokens_repository/tokens.json diff --git a/tests/test_rest_endpoints/data/jobs.json b/tests/test_rest_endpoints/data/jobs.json deleted file mode 100644 index 0967ef4..0000000 --- a/tests/test_rest_endpoints/data/jobs.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/test_rest_endpoints/services/data/gitkeep b/tests/test_rest_endpoints/services/data/gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_rest_endpoints/services/test_bitwarden.py b/tests/test_rest_endpoints/services/test_bitwarden.py deleted file mode 100644 index 3977253..0000000 --- a/tests/test_rest_endpoints/services/test_bitwarden.py +++ /dev/null @@ -1,125 +0,0 @@ -import json -import pytest - - -def read_json(file_path): - with open(file_path, "r") as f: - return json.load(f) - - -############################################################################### - - -@pytest.fixture -def bitwarden_off(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") - assert read_json(datadir / "turned_off.json")["bitwarden"]["enable"] == False - return datadir - - -@pytest.fixture -def bitwarden_on(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") - assert read_json(datadir / "turned_on.json")["bitwarden"]["enable"] == True - return datadir - - -@pytest.fixture -def bitwarden_enable_undefined(mocker, datadir): - mocker.patch( - "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" - ) - assert "enable" not in read_json(datadir / "enable_undefined.json")["bitwarden"] - return datadir - - -@pytest.fixture -def bitwarden_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") - assert "bitwarden" not in read_json(datadir / "undefined.json") - return datadir - - -############################################################################### - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_unauthorized(client, bitwarden_off, endpoint): - response = client.post(f"/services/bitwarden/{endpoint}") - assert response.status_code == 401 - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_illegal_methods(authorized_client, bitwarden_off, endpoint): - response = authorized_client.get(f"/services/bitwarden/{endpoint}") - assert response.status_code == 405 - response = authorized_client.put(f"/services/bitwarden/{endpoint}") - assert response.status_code == 405 - response = authorized_client.delete(f"/services/bitwarden/{endpoint}") - assert response.status_code == 405 - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_off(authorized_client, bitwarden_off, endpoint, target_file): - response = authorized_client.post(f"/services/bitwarden/{endpoint}") - assert response.status_code == 200 - assert read_json(bitwarden_off / "turned_off.json") == read_json( - bitwarden_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_on(authorized_client, bitwarden_on, endpoint, target_file): - response = authorized_client.post(f"/services/bitwarden/{endpoint}") - assert response.status_code == 200 - assert read_json(bitwarden_on / "turned_on.json") == read_json( - bitwarden_on / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_twice(authorized_client, bitwarden_off, endpoint, target_file): - response = authorized_client.post(f"/services/bitwarden/{endpoint}") - assert response.status_code == 200 - response = authorized_client.post(f"/services/bitwarden/{endpoint}") - assert response.status_code == 200 - assert read_json(bitwarden_off / "turned_off.json") == read_json( - bitwarden_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_on_attribute_deleted( - authorized_client, bitwarden_enable_undefined, endpoint, target_file -): - response = authorized_client.post(f"/services/bitwarden/{endpoint}") - assert response.status_code == 200 - assert read_json(bitwarden_enable_undefined / "enable_undefined.json") == read_json( - bitwarden_enable_undefined / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_on_bitwarden_undefined( - authorized_client, bitwarden_undefined, endpoint, target_file -): - response = authorized_client.post(f"/services/bitwarden/{endpoint}") - assert response.status_code == 200 - assert read_json(bitwarden_undefined / "undefined.json") == read_json( - bitwarden_undefined / target_file - ) diff --git a/tests/test_rest_endpoints/services/test_bitwarden/enable_undefined.json b/tests/test_rest_endpoints/services/test_bitwarden/enable_undefined.json deleted file mode 100644 index 1a95e85..0000000 --- a/tests/test_rest_endpoints/services/test_bitwarden/enable_undefined.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_bitwarden/turned_off.json b/tests/test_rest_endpoints/services/test_bitwarden/turned_off.json deleted file mode 100644 index c1691ea..0000000 --- a/tests/test_rest_endpoints/services/test_bitwarden/turned_off.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_bitwarden/turned_on.json b/tests/test_rest_endpoints/services/test_bitwarden/turned_on.json deleted file mode 100644 index 42999d8..0000000 --- a/tests/test_rest_endpoints/services/test_bitwarden/turned_on.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": true - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_bitwarden/undefined.json b/tests/test_rest_endpoints/services/test_bitwarden/undefined.json deleted file mode 100644 index ee288c2..0000000 --- a/tests/test_rest_endpoints/services/test_bitwarden/undefined.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_gitea.py b/tests/test_rest_endpoints/services/test_gitea.py deleted file mode 100644 index 0a50c19..0000000 --- a/tests/test_rest_endpoints/services/test_gitea.py +++ /dev/null @@ -1,121 +0,0 @@ -import json -import pytest - - -def read_json(file_path): - with open(file_path, "r") as f: - return json.load(f) - - -############################################################################### - - -@pytest.fixture -def gitea_off(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") - assert read_json(datadir / "turned_off.json")["gitea"]["enable"] == False - return datadir - - -@pytest.fixture -def gitea_on(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") - assert read_json(datadir / "turned_on.json")["gitea"]["enable"] == True - return datadir - - -@pytest.fixture -def gitea_enable_undefined(mocker, datadir): - mocker.patch( - "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" - ) - assert "enable" not in read_json(datadir / "enable_undefined.json")["gitea"] - return datadir - - -@pytest.fixture -def gitea_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") - assert "gitea" not in read_json(datadir / "undefined.json") - return datadir - - -############################################################################### - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_unauthorized(client, gitea_off, endpoint): - response = client.post(f"/services/gitea/{endpoint}") - assert response.status_code == 401 - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_illegal_methods(authorized_client, gitea_off, endpoint): - response = authorized_client.get(f"/services/gitea/{endpoint}") - assert response.status_code == 405 - response = authorized_client.put(f"/services/gitea/{endpoint}") - assert response.status_code == 405 - response = authorized_client.delete(f"/services/gitea/{endpoint}") - assert response.status_code == 405 - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_off(authorized_client, gitea_off, endpoint, target_file): - response = authorized_client.post(f"/services/gitea/{endpoint}") - assert response.status_code == 200 - assert read_json(gitea_off / "turned_off.json") == read_json( - gitea_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_on(authorized_client, gitea_on, endpoint, target_file): - response = authorized_client.post(f"/services/gitea/{endpoint}") - assert response.status_code == 200 - assert read_json(gitea_on / "turned_on.json") == read_json(gitea_on / target_file) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_twice(authorized_client, gitea_off, endpoint, target_file): - response = authorized_client.post(f"/services/gitea/{endpoint}") - assert response.status_code == 200 - response = authorized_client.post(f"/services/gitea/{endpoint}") - assert response.status_code == 200 - assert read_json(gitea_off / "turned_off.json") == read_json( - gitea_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_on_attribute_deleted( - authorized_client, gitea_enable_undefined, endpoint, target_file -): - response = authorized_client.post(f"/services/gitea/{endpoint}") - assert response.status_code == 200 - assert read_json(gitea_enable_undefined / "enable_undefined.json") == read_json( - gitea_enable_undefined / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_on_gitea_undefined(authorized_client, gitea_undefined, endpoint, target_file): - response = authorized_client.post(f"/services/gitea/{endpoint}") - assert response.status_code == 200 - assert read_json(gitea_undefined / "undefined.json") == read_json( - gitea_undefined / target_file - ) diff --git a/tests/test_rest_endpoints/services/test_gitea/enable_undefined.json b/tests/test_rest_endpoints/services/test_gitea/enable_undefined.json deleted file mode 100644 index f9fb878..0000000 --- a/tests/test_rest_endpoints/services/test_gitea/enable_undefined.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_gitea/turned_off.json b/tests/test_rest_endpoints/services/test_gitea/turned_off.json deleted file mode 100644 index c1691ea..0000000 --- a/tests/test_rest_endpoints/services/test_gitea/turned_off.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_gitea/turned_on.json b/tests/test_rest_endpoints/services/test_gitea/turned_on.json deleted file mode 100644 index f9a1eaf..0000000 --- a/tests/test_rest_endpoints/services/test_gitea/turned_on.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": true - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_gitea/undefined.json b/tests/test_rest_endpoints/services/test_gitea/undefined.json deleted file mode 100644 index a50a070..0000000 --- a/tests/test_rest_endpoints/services/test_gitea/undefined.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_mailserver.py b/tests/test_rest_endpoints/services/test_mailserver.py deleted file mode 100644 index 2803683..0000000 --- a/tests/test_rest_endpoints/services/test_mailserver.py +++ /dev/null @@ -1,102 +0,0 @@ -import base64 -import json -import pytest - -from selfprivacy_api.utils import get_dkim_key - -############################################################################### - - -class ProcessMock: - """Mock subprocess.Popen""" - - def __init__(self, args, **kwargs): - self.args = args - self.kwargs = kwargs - - def communicate(): - return ( - b'selector._domainkey\tIN\tTXT\t( "v=DKIM1; k=rsa; "\n\t "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNn/IhEz1SxgHxxxI8vlPYC2dNueiLe1GC4SYz8uHimC8SDkMvAwm7rqi2SimbFgGB5nccCNOqCkrIqJTCB9vufqBnVKAjshHqpOr5hk4JJ1T/AGQKWinstmDbfTLPYTbU8ijZrwwGeqQLlnXR5nSN0GB9GazheA9zaPsT6PV+aQIDAQAB" ) ; ----- DKIM key selector for example.com\n', - None, - ) - - -class NoFileMock(ProcessMock): - def communicate(): - return (b"", None) - - -@pytest.fixture -def mock_subproccess_popen(mocker): - mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock) - mocker.patch( - "selfprivacy_api.rest.services.get_domain", - autospec=True, - return_value="example.com", - ) - mocker.patch("os.path.exists", autospec=True, return_value=True) - return mock - - -@pytest.fixture -def mock_no_file(mocker): - mock = mocker.patch("subprocess.Popen", autospec=True, return_value=NoFileMock) - mocker.patch( - "selfprivacy_api.rest.services.get_domain", - autospec=True, - return_value="example.com", - ) - mocker.patch("os.path.exists", autospec=True, return_value=False) - return mock - - -############################################################################### - - -def test_unauthorized(client, mock_subproccess_popen): - """Test unauthorized""" - response = client.get("/services/mailserver/dkim") - assert response.status_code == 401 - - -def test_illegal_methods(authorized_client, mock_subproccess_popen): - response = authorized_client.post("/services/mailserver/dkim") - assert response.status_code == 405 - response = authorized_client.put("/services/mailserver/dkim") - assert response.status_code == 405 - response = authorized_client.delete("/services/mailserver/dkim") - assert response.status_code == 405 - - -def test_get_dkim_key(mock_subproccess_popen): - """Test DKIM key""" - dkim_key = get_dkim_key("example.com") - assert ( - dkim_key - == "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNn/IhEz1SxgHxxxI8vlPYC2dNueiLe1GC4SYz8uHimC8SDkMvAwm7rqi2SimbFgGB5nccCNOqCkrIqJTCB9vufqBnVKAjshHqpOr5hk4JJ1T/AGQKWinstmDbfTLPYTbU8ijZrwwGeqQLlnXR5nSN0GB9GazheA9zaPsT6PV+aQIDAQAB" - ) - assert mock_subproccess_popen.call_args[0][0] == [ - "cat", - "/var/dkim/example.com.selector.txt", - ] - - -def test_dkim_key(authorized_client, mock_subproccess_popen): - """Test old REST DKIM key endpoint""" - response = authorized_client.get("/services/mailserver/dkim") - assert response.status_code == 200 - assert ( - base64.b64decode(response.text) - == b'selector._domainkey\tIN\tTXT\t( "v=DKIM1; k=rsa; "\n\t "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNn/IhEz1SxgHxxxI8vlPYC2dNueiLe1GC4SYz8uHimC8SDkMvAwm7rqi2SimbFgGB5nccCNOqCkrIqJTCB9vufqBnVKAjshHqpOr5hk4JJ1T/AGQKWinstmDbfTLPYTbU8ijZrwwGeqQLlnXR5nSN0GB9GazheA9zaPsT6PV+aQIDAQAB" ) ; ----- DKIM key selector for example.com\n' - ) - assert mock_subproccess_popen.call_args[0][0] == [ - "cat", - "/var/dkim/example.com.selector.txt", - ] - - -def test_no_dkim_key(authorized_client, mock_no_file): - """Test no DKIM key""" - response = authorized_client.get("/services/mailserver/dkim") - assert response.status_code == 404 - assert mock_no_file.called == False diff --git a/tests/test_rest_endpoints/services/test_nextcloud.py b/tests/test_rest_endpoints/services/test_nextcloud.py deleted file mode 100644 index b05c363..0000000 --- a/tests/test_rest_endpoints/services/test_nextcloud.py +++ /dev/null @@ -1,123 +0,0 @@ -import json -import pytest - - -def read_json(file_path): - with open(file_path, "r") as f: - return json.load(f) - - -############################################################################### - - -@pytest.fixture -def nextcloud_off(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") - assert read_json(datadir / "turned_off.json")["nextcloud"]["enable"] == False - return datadir - - -@pytest.fixture -def nextcloud_on(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") - assert read_json(datadir / "turned_on.json")["nextcloud"]["enable"] == True - return datadir - - -@pytest.fixture -def nextcloud_enable_undefined(mocker, datadir): - mocker.patch( - "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" - ) - assert "enable" not in read_json(datadir / "enable_undefined.json")["nextcloud"] - return datadir - - -@pytest.fixture -def nextcloud_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") - assert "nextcloud" not in read_json(datadir / "undefined.json") - return datadir - - -############################################################################### - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_unauthorized(client, nextcloud_off, endpoint): - response = client.post(f"/services/nextcloud/{endpoint}") - assert response.status_code == 401 - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_illegal_methods(authorized_client, nextcloud_off, endpoint): - response = authorized_client.get(f"/services/nextcloud/{endpoint}") - assert response.status_code == 405 - response = authorized_client.put(f"/services/nextcloud/{endpoint}") - assert response.status_code == 405 - response = authorized_client.delete(f"/services/nextcloud/{endpoint}") - assert response.status_code == 405 - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_off(authorized_client, nextcloud_off, endpoint, target_file): - response = authorized_client.post(f"/services/nextcloud/{endpoint}") - assert response.status_code == 200 - assert read_json(nextcloud_off / "turned_off.json") == read_json( - nextcloud_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_on(authorized_client, nextcloud_on, endpoint, target_file): - response = authorized_client.post(f"/services/nextcloud/{endpoint}") - assert response.status_code == 200 - assert read_json(nextcloud_on / "turned_on.json") == read_json( - nextcloud_on / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_twice(authorized_client, nextcloud_off, endpoint, target_file): - response = authorized_client.post(f"/services/nextcloud/{endpoint}") - assert response.status_code == 200 - response = authorized_client.post(f"/services/nextcloud/{endpoint}") - assert response.status_code == 200 - assert read_json(nextcloud_off / "turned_off.json") == read_json( - nextcloud_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_on_attribute_deleted( - authorized_client, nextcloud_enable_undefined, endpoint, target_file -): - response = authorized_client.post(f"/services/nextcloud/{endpoint}") - assert response.status_code == 200 - assert read_json(nextcloud_enable_undefined / "enable_undefined.json") == read_json( - nextcloud_enable_undefined / target_file - ) - - -@pytest.mark.parametrize("endpoint,target", [("enable", True), ("disable", False)]) -def test_on_nextcloud_undefined( - authorized_client, nextcloud_undefined, endpoint, target -): - response = authorized_client.post(f"/services/nextcloud/{endpoint}") - assert response.status_code == 200 - assert ( - read_json(nextcloud_undefined / "undefined.json")["nextcloud"]["enable"] - == target - ) diff --git a/tests/test_rest_endpoints/services/test_nextcloud/enable_undefined.json b/tests/test_rest_endpoints/services/test_nextcloud/enable_undefined.json deleted file mode 100644 index 19f1f2d..0000000 --- a/tests/test_rest_endpoints/services/test_nextcloud/enable_undefined.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN" - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_nextcloud/turned_off.json b/tests/test_rest_endpoints/services/test_nextcloud/turned_off.json deleted file mode 100644 index b80ad9e..0000000 --- a/tests/test_rest_endpoints/services/test_nextcloud/turned_off.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": false - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_nextcloud/turned_on.json b/tests/test_rest_endpoints/services/test_nextcloud/turned_on.json deleted file mode 100644 index c1691ea..0000000 --- a/tests/test_rest_endpoints/services/test_nextcloud/turned_on.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_nextcloud/undefined.json b/tests/test_rest_endpoints/services/test_nextcloud/undefined.json deleted file mode 100644 index 46c09f3..0000000 --- a/tests/test_rest_endpoints/services/test_nextcloud/undefined.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ocserv.py b/tests/test_rest_endpoints/services/test_ocserv.py deleted file mode 100644 index 8f43e70..0000000 --- a/tests/test_rest_endpoints/services/test_ocserv.py +++ /dev/null @@ -1,123 +0,0 @@ -import json -import pytest - - -def read_json(file_path): - with open(file_path, "r") as f: - return json.load(f) - - -############################################################################### - - -@pytest.fixture -def ocserv_off(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") - assert read_json(datadir / "turned_off.json")["ocserv"]["enable"] == False - return datadir - - -@pytest.fixture -def ocserv_on(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") - assert read_json(datadir / "turned_on.json")["ocserv"]["enable"] == True - return datadir - - -@pytest.fixture -def ocserv_enable_undefined(mocker, datadir): - mocker.patch( - "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" - ) - assert "enable" not in read_json(datadir / "enable_undefined.json")["ocserv"] - return datadir - - -@pytest.fixture -def ocserv_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") - assert "ocserv" not in read_json(datadir / "undefined.json") - return datadir - - -############################################################################### - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_unauthorized(client, ocserv_off, endpoint): - response = client.post(f"/services/ocserv/{endpoint}") - assert response.status_code == 401 - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_illegal_methods(authorized_client, ocserv_off, endpoint): - response = authorized_client.get(f"/services/ocserv/{endpoint}") - assert response.status_code == 405 - response = authorized_client.put(f"/services/ocserv/{endpoint}") - assert response.status_code == 405 - response = authorized_client.delete(f"/services/ocserv/{endpoint}") - assert response.status_code == 405 - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_off(authorized_client, ocserv_off, endpoint, target_file): - response = authorized_client.post(f"/services/ocserv/{endpoint}") - assert response.status_code == 200 - assert read_json(ocserv_off / "turned_off.json") == read_json( - ocserv_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_on(authorized_client, ocserv_on, endpoint, target_file): - response = authorized_client.post(f"/services/ocserv/{endpoint}") - assert response.status_code == 200 - assert read_json(ocserv_on / "turned_on.json") == read_json(ocserv_on / target_file) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_twice(authorized_client, ocserv_off, endpoint, target_file): - response = authorized_client.post(f"/services/ocserv/{endpoint}") - assert response.status_code == 200 - response = authorized_client.post(f"/services/ocserv/{endpoint}") - assert response.status_code == 200 - assert read_json(ocserv_off / "turned_off.json") == read_json( - ocserv_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_on_attribute_deleted( - authorized_client, ocserv_enable_undefined, endpoint, target_file -): - response = authorized_client.post(f"/services/ocserv/{endpoint}") - assert response.status_code == 200 - assert read_json(ocserv_enable_undefined / "enable_undefined.json") == read_json( - ocserv_enable_undefined / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_on_ocserv_undefined( - authorized_client, ocserv_undefined, endpoint, target_file -): - response = authorized_client.post(f"/services/ocserv/{endpoint}") - assert response.status_code == 200 - assert read_json(ocserv_undefined / "undefined.json") == read_json( - ocserv_undefined / target_file - ) diff --git a/tests/test_rest_endpoints/services/test_ocserv/enable_undefined.json b/tests/test_rest_endpoints/services/test_ocserv/enable_undefined.json deleted file mode 100644 index e080110..0000000 --- a/tests/test_rest_endpoints/services/test_ocserv/enable_undefined.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": false - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ocserv/turned_off.json b/tests/test_rest_endpoints/services/test_ocserv/turned_off.json deleted file mode 100644 index 1c08123..0000000 --- a/tests/test_rest_endpoints/services/test_ocserv/turned_off.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": false - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": false - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ocserv/turned_on.json b/tests/test_rest_endpoints/services/test_ocserv/turned_on.json deleted file mode 100644 index b80ad9e..0000000 --- a/tests/test_rest_endpoints/services/test_ocserv/turned_on.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": false - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ocserv/undefined.json b/tests/test_rest_endpoints/services/test_ocserv/undefined.json deleted file mode 100644 index 12eb73a..0000000 --- a/tests/test_rest_endpoints/services/test_ocserv/undefined.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": false - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_pleroma.py b/tests/test_rest_endpoints/services/test_pleroma.py deleted file mode 100644 index 0d7f149..0000000 --- a/tests/test_rest_endpoints/services/test_pleroma.py +++ /dev/null @@ -1,125 +0,0 @@ -import json -import pytest - - -def read_json(file_path): - with open(file_path, "r") as f: - return json.load(f) - - -############################################################################### - - -@pytest.fixture -def pleroma_off(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") - assert read_json(datadir / "turned_off.json")["pleroma"]["enable"] == False - return datadir - - -@pytest.fixture -def pleroma_on(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") - assert read_json(datadir / "turned_on.json")["pleroma"]["enable"] == True - return datadir - - -@pytest.fixture -def pleroma_enable_undefined(mocker, datadir): - mocker.patch( - "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "enable_undefined.json" - ) - assert "enable" not in read_json(datadir / "enable_undefined.json")["pleroma"] - return datadir - - -@pytest.fixture -def pleroma_undefined(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") - assert "pleroma" not in read_json(datadir / "undefined.json") - return datadir - - -############################################################################### - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_unauthorized(client, pleroma_off, endpoint): - response = client.post(f"/services/pleroma/{endpoint}") - assert response.status_code == 401 - - -@pytest.mark.parametrize("endpoint", ["enable", "disable"]) -def test_illegal_methods(authorized_client, pleroma_off, endpoint): - response = authorized_client.get(f"/services/pleroma/{endpoint}") - assert response.status_code == 405 - response = authorized_client.put(f"/services/pleroma/{endpoint}") - assert response.status_code == 405 - response = authorized_client.delete(f"/services/pleroma/{endpoint}") - assert response.status_code == 405 - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_off(authorized_client, pleroma_off, endpoint, target_file): - response = authorized_client.post(f"/services/pleroma/{endpoint}") - assert response.status_code == 200 - assert read_json(pleroma_off / "turned_off.json") == read_json( - pleroma_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_from_on(authorized_client, pleroma_on, endpoint, target_file): - response = authorized_client.post(f"/services/pleroma/{endpoint}") - assert response.status_code == 200 - assert read_json(pleroma_on / "turned_on.json") == read_json( - pleroma_on / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_switch_twice(authorized_client, pleroma_off, endpoint, target_file): - response = authorized_client.post(f"/services/pleroma/{endpoint}") - assert response.status_code == 200 - response = authorized_client.post(f"/services/pleroma/{endpoint}") - assert response.status_code == 200 - assert read_json(pleroma_off / "turned_off.json") == read_json( - pleroma_off / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_on_attribute_deleted( - authorized_client, pleroma_enable_undefined, endpoint, target_file -): - response = authorized_client.post(f"/services/pleroma/{endpoint}") - assert response.status_code == 200 - assert read_json(pleroma_enable_undefined / "enable_undefined.json") == read_json( - pleroma_enable_undefined / target_file - ) - - -@pytest.mark.parametrize( - "endpoint,target_file", - [("enable", "turned_on.json"), ("disable", "turned_off.json")], -) -def test_on_pleroma_undefined( - authorized_client, pleroma_undefined, endpoint, target_file -): - response = authorized_client.post(f"/services/pleroma/{endpoint}") - assert response.status_code == 200 - assert read_json(pleroma_undefined / "undefined.json") == read_json( - pleroma_undefined / target_file - ) diff --git a/tests/test_rest_endpoints/services/test_pleroma/enable_undefined.json b/tests/test_rest_endpoints/services/test_pleroma/enable_undefined.json deleted file mode 100644 index 0903875..0000000 --- a/tests/test_rest_endpoints/services/test_pleroma/enable_undefined.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": false - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": false - }, - "pleroma": { - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_pleroma/turned_off.json b/tests/test_rest_endpoints/services/test_pleroma/turned_off.json deleted file mode 100644 index 813c01f..0000000 --- a/tests/test_rest_endpoints/services/test_pleroma/turned_off.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": false - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": false - }, - "pleroma": { - "enable": false - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_pleroma/turned_on.json b/tests/test_rest_endpoints/services/test_pleroma/turned_on.json deleted file mode 100644 index 1c08123..0000000 --- a/tests/test_rest_endpoints/services/test_pleroma/turned_on.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": false - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": false - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_pleroma/undefined.json b/tests/test_rest_endpoints/services/test_pleroma/undefined.json deleted file mode 100644 index 77d8ad2..0000000 --- a/tests/test_rest_endpoints/services/test_pleroma/undefined.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": false - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": false - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_restic/no_values.json b/tests/test_rest_endpoints/services/test_restic/no_values.json deleted file mode 100644 index 3b4a2f5..0000000 --- a/tests/test_rest_endpoints/services/test_restic/no_values.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "users": [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": [ - "ssh-rsa KEY user1@pc" - ] - }, - { - "username": "user2", - "hashedPassword": "HASHED_PASSWORD_2", - "sshKeys": [ - ] - }, - { - "username": "user3", - "hashedPassword": "HASHED_PASSWORD_3" - } - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_restic/some_values.json b/tests/test_rest_endpoints/services/test_restic/some_values.json deleted file mode 100644 index c003d10..0000000 --- a/tests/test_rest_endpoints/services/test_restic/some_values.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "users": [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": [ - "ssh-rsa KEY user1@pc" - ] - }, - { - "username": "user2", - "hashedPassword": "HASHED_PASSWORD_2", - "sshKeys": [ - ] - }, - { - "username": "user3", - "hashedPassword": "HASHED_PASSWORD_3" - } - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "BUCKET" - } -} diff --git a/tests/test_rest_endpoints/services/test_restic/undefined.json b/tests/test_rest_endpoints/services/test_restic/undefined.json deleted file mode 100644 index 5bd1220..0000000 --- a/tests/test_rest_endpoints/services/test_restic/undefined.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "users": [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": [ - "ssh-rsa KEY user1@pc" - ] - }, - { - "username": "user2", - "hashedPassword": "HASHED_PASSWORD_2", - "sshKeys": [ - ] - }, - { - "username": "user3", - "hashedPassword": "HASHED_PASSWORD_3" - } - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_services.py b/tests/test_rest_endpoints/services/test_services.py deleted file mode 100644 index 1108e8c..0000000 --- a/tests/test_rest_endpoints/services/test_services.py +++ /dev/null @@ -1,138 +0,0 @@ -import base64 -import json -import pytest - - -def read_json(file_path): - with open(file_path, "r", encoding="utf-8") as file: - return json.load(file) - - -def call_args_asserts(mocked_object): - assert mocked_object.call_count == 7 - assert mocked_object.call_args_list[0][0][0] == [ - "systemctl", - "show", - "dovecot2.service", - ] - assert mocked_object.call_args_list[1][0][0] == [ - "systemctl", - "show", - "postfix.service", - ] - assert mocked_object.call_args_list[2][0][0] == [ - "systemctl", - "show", - "vaultwarden.service", - ] - assert mocked_object.call_args_list[3][0][0] == [ - "systemctl", - "show", - "gitea.service", - ] - assert mocked_object.call_args_list[4][0][0] == [ - "systemctl", - "show", - "phpfpm-nextcloud.service", - ] - assert mocked_object.call_args_list[5][0][0] == [ - "systemctl", - "show", - "ocserv.service", - ] - assert mocked_object.call_args_list[6][0][0] == [ - "systemctl", - "show", - "pleroma.service", - ] - - -SUCCESSFUL_STATUS = b""" -Type=oneshot -ExitType=main -Restart=no -NotifyAccess=none -RestartUSec=100ms -LoadState=loaded -ActiveState=active -FreezerState=running -SubState=exited -""" - -FAILED_STATUS = b""" -Type=oneshot -ExitType=main -Restart=no -NotifyAccess=none -RestartUSec=100ms -LoadState=loaded -ActiveState=failed -FreezerState=running -SubState=exited -""" - - -@pytest.fixture -def mock_subproccess_popen(mocker): - mock = mocker.patch( - "subprocess.check_output", autospec=True, return_value=SUCCESSFUL_STATUS - ) - return mock - - -@pytest.fixture -def mock_broken_service(mocker): - mock = mocker.patch( - "subprocess.check_output", autospec=True, return_value=FAILED_STATUS - ) - return mock - - -############################################################################### - - -def test_unauthorized(client, mock_subproccess_popen): - """Test unauthorized""" - response = client.get("/services/status") - assert response.status_code == 401 - - -def test_illegal_methods(authorized_client, mock_subproccess_popen): - response = authorized_client.post("/services/status") - assert response.status_code == 405 - response = authorized_client.put("/services/status") - assert response.status_code == 405 - response = authorized_client.delete("/services/status") - assert response.status_code == 405 - - -def test_dkim_key(authorized_client, mock_subproccess_popen): - response = authorized_client.get("/services/status") - assert response.status_code == 200 - assert response.json() == { - "imap": 0, - "smtp": 0, - "http": 0, - "bitwarden": 0, - "gitea": 0, - "nextcloud": 0, - "ocserv": 0, - "pleroma": 0, - } - call_args_asserts(mock_subproccess_popen) - - -def test_no_dkim_key(authorized_client, mock_broken_service): - response = authorized_client.get("/services/status") - assert response.status_code == 200 - assert response.json() == { - "imap": 1, - "smtp": 1, - "http": 0, - "bitwarden": 1, - "gitea": 1, - "nextcloud": 1, - "ocserv": 1, - "pleroma": 1, - } - call_args_asserts(mock_broken_service) diff --git a/tests/test_rest_endpoints/services/test_ssh.py b/tests/test_rest_endpoints/services/test_ssh.py deleted file mode 100644 index a17bdab..0000000 --- a/tests/test_rest_endpoints/services/test_ssh.py +++ /dev/null @@ -1,521 +0,0 @@ -# pylint: disable=redefined-outer-name -# pylint: disable=unused-argument -import json -import pytest - - -def read_json(file_path): - with open(file_path, "r", encoding="utf-8") as file: - return json.load(file) - - -## FIXTURES ################################################### - - -@pytest.fixture -def ssh_off(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") - assert not read_json(datadir / "turned_off.json")["ssh"]["enable"] - assert read_json(datadir / "turned_off.json")["ssh"]["passwordAuthentication"] - return datadir - - -@pytest.fixture -def ssh_on(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") - assert read_json(datadir / "turned_off.json")["ssh"]["passwordAuthentication"] - assert read_json(datadir / "turned_on.json")["ssh"]["enable"] - return datadir - - -@pytest.fixture -def all_off(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "all_off.json") - assert not read_json(datadir / "all_off.json")["ssh"]["passwordAuthentication"] - assert not read_json(datadir / "all_off.json")["ssh"]["enable"] - return datadir - - -@pytest.fixture -def undefined_settings(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") - assert "ssh" not in read_json(datadir / "undefined.json") - return datadir - - -@pytest.fixture -def undefined_values(mocker, datadir): - mocker.patch( - "selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined_values.json" - ) - assert "ssh" in read_json(datadir / "undefined_values.json") - assert "enable" not in read_json(datadir / "undefined_values.json")["ssh"] - assert ( - "passwordAuthentication" - not in read_json(datadir / "undefined_values.json")["ssh"] - ) - return datadir - - -@pytest.fixture -def root_and_admin_have_keys(mocker, datadir): - mocker.patch( - "selfprivacy_api.utils.USERDATA_FILE", - new=datadir / "root_and_admin_have_keys.json", - ) - assert read_json(datadir / "root_and_admin_have_keys.json")["ssh"]["enable"] - assert read_json(datadir / "root_and_admin_have_keys.json")["ssh"][ - "passwordAuthentication" - ] - assert read_json(datadir / "root_and_admin_have_keys.json")["ssh"]["rootKeys"] == [ - "ssh-ed25519 KEY test@pc" - ] - assert read_json(datadir / "root_and_admin_have_keys.json")["sshKeys"] == [ - "ssh-rsa KEY test@pc" - ] - return datadir - - -@pytest.fixture -def some_users(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "some_users.json") - assert "users" in read_json(datadir / "some_users.json") - assert read_json(datadir / "some_users.json")["users"] == [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": ["ssh-rsa KEY user1@pc"], - }, - {"username": "user2", "hashedPassword": "HASHED_PASSWORD_2", "sshKeys": []}, - {"username": "user3", "hashedPassword": "HASHED_PASSWORD_3"}, - ] - return datadir - - -## TEST 401 ###################################################### - - -@pytest.mark.parametrize("endpoint", ["ssh/enable", "ssh/keys/user"]) -def test_unauthorized(client, ssh_off, endpoint): - response = client.post(f"/services/{endpoint}") - assert response.status_code == 401 - - -@pytest.mark.parametrize("endpoint", ["ssh", "ssh/key/send"]) -def test_unauthorized_put(client, ssh_off, endpoint): - response = client.put(f"/services/{endpoint}") - assert response.status_code == 401 - - -## TEST ENABLE ###################################################### - - -def test_legacy_enable(authorized_client, ssh_off): - response = authorized_client.post("/services/ssh/enable") - assert response.status_code == 200 - assert read_json(ssh_off / "turned_off.json") == read_json( - ssh_off / "turned_on.json" - ) - - -def test_legacy_on_undefined(authorized_client, undefined_settings): - response = authorized_client.post("/services/ssh/enable") - assert response.status_code == 200 - data = read_json(undefined_settings / "undefined.json") - assert data["ssh"]["enable"] == True - - -def test_legacy_enable_when_enabled(authorized_client, ssh_on): - response = authorized_client.post("/services/ssh/enable") - assert response.status_code == 200 - assert read_json(ssh_on / "turned_on.json") == read_json(ssh_on / "turned_on.json") - - -## GET ON /ssh ###################################################### - - -def test_get_current_settings_ssh_off(authorized_client, ssh_off): - response = authorized_client.get("/services/ssh") - assert response.status_code == 200 - assert response.json() == {"enable": False, "passwordAuthentication": True} - - -def test_get_current_settings_ssh_on(authorized_client, ssh_on): - response = authorized_client.get("/services/ssh") - assert response.status_code == 200 - assert response.json() == {"enable": True, "passwordAuthentication": True} - - -def test_get_current_settings_all_off(authorized_client, all_off): - response = authorized_client.get("/services/ssh") - assert response.status_code == 200 - assert response.json() == {"enable": False, "passwordAuthentication": False} - - -def test_get_current_settings_undefined(authorized_client, undefined_settings): - response = authorized_client.get("/services/ssh") - assert response.status_code == 200 - assert response.json() == {"enable": True, "passwordAuthentication": True} - - -def test_get_current_settings_mostly_undefined(authorized_client, undefined_values): - response = authorized_client.get("/services/ssh") - assert response.status_code == 200 - assert response.json() == {"enable": True, "passwordAuthentication": True} - - -## PUT ON /ssh ###################################################### - -available_settings = [ - {"enable": True, "passwordAuthentication": True}, - {"enable": True, "passwordAuthentication": False}, - {"enable": False, "passwordAuthentication": True}, - {"enable": False, "passwordAuthentication": False}, - {"enable": True}, - {"enable": False}, - {"passwordAuthentication": True}, - {"passwordAuthentication": False}, -] - - -@pytest.mark.parametrize("settings", available_settings) -def test_set_settings_ssh_off(authorized_client, ssh_off, settings): - response = authorized_client.put("/services/ssh", json=settings) - assert response.status_code == 200 - data = read_json(ssh_off / "turned_off.json")["ssh"] - if "enable" in settings: - assert data["enable"] == settings["enable"] - if "passwordAuthentication" in settings: - assert data["passwordAuthentication"] == settings["passwordAuthentication"] - - -@pytest.mark.parametrize("settings", available_settings) -def test_set_settings_ssh_on(authorized_client, ssh_on, settings): - response = authorized_client.put("/services/ssh", json=settings) - assert response.status_code == 200 - data = read_json(ssh_on / "turned_on.json")["ssh"] - if "enable" in settings: - assert data["enable"] == settings["enable"] - if "passwordAuthentication" in settings: - assert data["passwordAuthentication"] == settings["passwordAuthentication"] - - -@pytest.mark.parametrize("settings", available_settings) -def test_set_settings_all_off(authorized_client, all_off, settings): - response = authorized_client.put("/services/ssh", json=settings) - assert response.status_code == 200 - data = read_json(all_off / "all_off.json")["ssh"] - if "enable" in settings: - assert data["enable"] == settings["enable"] - if "passwordAuthentication" in settings: - assert data["passwordAuthentication"] == settings["passwordAuthentication"] - - -@pytest.mark.parametrize("settings", available_settings) -def test_set_settings_undefined(authorized_client, undefined_settings, settings): - response = authorized_client.put("/services/ssh", json=settings) - assert response.status_code == 200 - data = read_json(undefined_settings / "undefined.json")["ssh"] - if "enable" in settings: - assert data["enable"] == settings["enable"] - if "passwordAuthentication" in settings: - assert data["passwordAuthentication"] == settings["passwordAuthentication"] - - -## PUT ON /ssh/key/send ###################################################### - - -def test_add_root_key(authorized_client, ssh_on): - response = authorized_client.put( - "/services/ssh/key/send", json={"public_key": "ssh-rsa KEY test@pc"} - ) - assert response.status_code == 201 - assert read_json(ssh_on / "turned_on.json")["ssh"]["rootKeys"] == [ - "ssh-rsa KEY test@pc", - ] - - -def test_add_root_key_on_undefined(authorized_client, undefined_settings): - response = authorized_client.put( - "/services/ssh/key/send", json={"public_key": "ssh-rsa KEY test@pc"} - ) - assert response.status_code == 201 - data = read_json(undefined_settings / "undefined.json") - assert data["ssh"]["rootKeys"] == ["ssh-rsa KEY test@pc"] - - -def test_add_root_key_one_more(authorized_client, root_and_admin_have_keys): - response = authorized_client.put( - "/services/ssh/key/send", json={"public_key": "ssh-rsa KEY test@pc"} - ) - assert response.status_code == 201 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"][ - "rootKeys" - ] == [ - "ssh-ed25519 KEY test@pc", - "ssh-rsa KEY test@pc", - ] - - -def test_add_existing_root_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.put( - "/services/ssh/key/send", json={"public_key": "ssh-ed25519 KEY test@pc"} - ) - assert response.status_code == 409 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"][ - "rootKeys" - ] == [ - "ssh-ed25519 KEY test@pc", - ] - - -def test_add_invalid_root_key(authorized_client, ssh_on): - response = authorized_client.put( - "/services/ssh/key/send", json={"public_key": "INVALID KEY test@pc"} - ) - assert response.status_code == 400 - - -## /ssh/keys/{user} ###################################################### - - -def test_get_root_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.get("/services/ssh/keys/root") - assert response.status_code == 200 - assert response.json() == ["ssh-ed25519 KEY test@pc"] - - -def test_get_root_key_when_none(authorized_client, ssh_on): - response = authorized_client.get("/services/ssh/keys/root") - assert response.status_code == 200 - assert response.json() == [] - - -def test_get_root_key_on_undefined(authorized_client, undefined_settings): - response = authorized_client.get("/services/ssh/keys/root") - assert response.status_code == 200 - assert response.json() == [] - - -def test_delete_root_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.delete( - "/services/ssh/keys/root", json={"public_key": "ssh-ed25519 KEY test@pc"} - ) - assert response.status_code == 200 - assert ( - "rootKeys" - not in read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")[ - "ssh" - ] - or read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"][ - "rootKeys" - ] - == [] - ) - - -def test_delete_root_nonexistent_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.delete( - "/services/ssh/keys/root", json={"public_key": "ssh-rsa KEY test@pc"} - ) - assert response.status_code == 404 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["ssh"][ - "rootKeys" - ] == [ - "ssh-ed25519 KEY test@pc", - ] - - -def test_delete_root_key_on_undefined(authorized_client, undefined_settings): - response = authorized_client.delete( - "/services/ssh/keys/root", json={"public_key": "ssh-ed25519 KEY test@pc"} - ) - assert response.status_code == 404 - assert "ssh" not in read_json(undefined_settings / "undefined.json") - - -def test_get_admin_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.get("/services/ssh/keys/tester") - assert response.status_code == 200 - assert response.json() == ["ssh-rsa KEY test@pc"] - - -def test_get_admin_key_when_none(authorized_client, ssh_on): - response = authorized_client.get("/services/ssh/keys/tester") - assert response.status_code == 200 - assert response.json() == [] - - -def test_delete_admin_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.delete( - "/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"} - ) - assert response.status_code == 200 - assert ( - read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")["sshKeys"] - == [] - ) - - -def test_delete_nonexistent_admin_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.delete( - "/services/ssh/keys/tester", json={"public_key": "ssh-rsa NO KEY test@pc"} - ) - assert response.status_code == 404 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")[ - "sshKeys" - ] == ["ssh-rsa KEY test@pc"] - - -def test_delete_admin_key_on_undefined(authorized_client, undefined_settings): - response = authorized_client.delete( - "/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"} - ) - assert response.status_code == 404 - assert "sshKeys" not in read_json(undefined_settings / "undefined.json") - - -def test_add_admin_key(authorized_client, ssh_on): - response = authorized_client.post( - "/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"} - ) - assert response.status_code == 201 - assert read_json(ssh_on / "turned_on.json")["sshKeys"] == [ - "ssh-rsa KEY test@pc", - ] - - -def test_add_admin_key_one_more(authorized_client, root_and_admin_have_keys): - response = authorized_client.post( - "/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY_2 test@pc"} - ) - assert response.status_code == 201 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")[ - "sshKeys" - ] == ["ssh-rsa KEY test@pc", "ssh-rsa KEY_2 test@pc"] - - -def test_add_existing_admin_key(authorized_client, root_and_admin_have_keys): - response = authorized_client.post( - "/services/ssh/keys/tester", json={"public_key": "ssh-rsa KEY test@pc"} - ) - assert response.status_code == 409 - assert read_json(root_and_admin_have_keys / "root_and_admin_have_keys.json")[ - "sshKeys" - ] == [ - "ssh-rsa KEY test@pc", - ] - - -def test_add_invalid_admin_key(authorized_client, ssh_on): - response = authorized_client.post( - "/services/ssh/keys/tester", json={"public_key": "INVALID KEY test@pc"} - ) - assert response.status_code == 400 - - -@pytest.mark.parametrize("user", [1, 2, 3]) -def test_get_user_key(authorized_client, some_users, user): - response = authorized_client.get(f"/services/ssh/keys/user{user}") - assert response.status_code == 200 - if user == 1: - assert response.json() == ["ssh-rsa KEY user1@pc"] - else: - assert response.json() == [] - - -def test_get_keys_of_nonexistent_user(authorized_client, some_users): - response = authorized_client.get("/services/ssh/keys/user4") - assert response.status_code == 404 - - -def test_get_keys_of_undefined_users(authorized_client, undefined_settings): - response = authorized_client.get("/services/ssh/keys/user1") - assert response.status_code == 404 - - -@pytest.mark.parametrize("user", [1, 2, 3]) -def test_add_user_key(authorized_client, some_users, user): - response = authorized_client.post( - f"/services/ssh/keys/user{user}", json={"public_key": "ssh-ed25519 KEY test@pc"} - ) - assert response.status_code == 201 - if user == 1: - assert read_json(some_users / "some_users.json")["users"][user - 1][ - "sshKeys" - ] == [ - "ssh-rsa KEY user1@pc", - "ssh-ed25519 KEY test@pc", - ] - else: - assert read_json(some_users / "some_users.json")["users"][user - 1][ - "sshKeys" - ] == ["ssh-ed25519 KEY test@pc"] - - -def test_add_existing_user_key(authorized_client, some_users): - response = authorized_client.post( - "/services/ssh/keys/user1", json={"public_key": "ssh-rsa KEY user1@pc"} - ) - assert response.status_code == 409 - assert read_json(some_users / "some_users.json")["users"][0]["sshKeys"] == [ - "ssh-rsa KEY user1@pc", - ] - - -def test_add_invalid_user_key(authorized_client, some_users): - response = authorized_client.post( - "/services/ssh/keys/user1", json={"public_key": "INVALID KEY user1@pc"} - ) - assert response.status_code == 400 - - -def test_delete_user_key(authorized_client, some_users): - response = authorized_client.delete( - "/services/ssh/keys/user1", json={"public_key": "ssh-rsa KEY user1@pc"} - ) - assert response.status_code == 200 - assert read_json(some_users / "some_users.json")["users"][0]["sshKeys"] == [] - - -@pytest.mark.parametrize("user", [2, 3]) -def test_delete_nonexistent_user_key(authorized_client, some_users, user): - response = authorized_client.delete( - f"/services/ssh/keys/user{user}", json={"public_key": "ssh-rsa KEY user1@pc"} - ) - assert response.status_code == 404 - if user == 2: - assert ( - read_json(some_users / "some_users.json")["users"][user - 1]["sshKeys"] - == [] - ) - if user == 3: - "sshKeys" not in read_json(some_users / "some_users.json")["users"][user - 1] - - -def test_add_keys_of_nonexistent_user(authorized_client, some_users): - response = authorized_client.post( - "/services/ssh/keys/user4", json={"public_key": "ssh-rsa KEY user4@pc"} - ) - assert response.status_code == 404 - - -def test_add_key_on_undefined_users(authorized_client, undefined_settings): - response = authorized_client.post( - "/services/ssh/keys/user1", json={"public_key": "ssh-rsa KEY user4@pc"} - ) - assert response.status_code == 404 - - -def test_delete_keys_of_nonexistent_user(authorized_client, some_users): - response = authorized_client.delete( - "/services/ssh/keys/user4", json={"public_key": "ssh-rsa KEY user4@pc"} - ) - assert response.status_code == 404 - - -def test_delete_key_when_undefined_users(authorized_client, undefined_settings): - response = authorized_client.delete( - "/services/ssh/keys/user1", json={"public_key": "ssh-rsa KEY user1@pc"} - ) - assert response.status_code == 404 diff --git a/tests/test_rest_endpoints/services/test_ssh/all_off.json b/tests/test_rest_endpoints/services/test_ssh/all_off.json deleted file mode 100644 index 051d364..0000000 --- a/tests/test_rest_endpoints/services/test_ssh/all_off.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": false, - "passwordAuthentication": false, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ssh/root_and_admin_have_keys.json b/tests/test_rest_endpoints/services/test_ssh/root_and_admin_have_keys.json deleted file mode 100644 index c1691ea..0000000 --- a/tests/test_rest_endpoints/services/test_ssh/root_and_admin_have_keys.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ssh/some_users.json b/tests/test_rest_endpoints/services/test_ssh/some_users.json deleted file mode 100644 index df6380a..0000000 --- a/tests/test_rest_endpoints/services/test_ssh/some_users.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "users": [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": [ - "ssh-rsa KEY user1@pc" - ] - }, - { - "username": "user2", - "hashedPassword": "HASHED_PASSWORD_2", - "sshKeys": [ - ] - }, - { - "username": "user3", - "hashedPassword": "HASHED_PASSWORD_3" - } - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ssh/turned_off.json b/tests/test_rest_endpoints/services/test_ssh/turned_off.json deleted file mode 100644 index 3856c80..0000000 --- a/tests/test_rest_endpoints/services/test_ssh/turned_off.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": false, - "passwordAuthentication": true - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ssh/turned_on.json b/tests/test_rest_endpoints/services/test_ssh/turned_on.json deleted file mode 100644 index e60c57f..0000000 --- a/tests/test_rest_endpoints/services/test_ssh/turned_on.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ssh/undefined.json b/tests/test_rest_endpoints/services/test_ssh/undefined.json deleted file mode 100644 index 7c9af37..0000000 --- a/tests/test_rest_endpoints/services/test_ssh/undefined.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/test_ssh/undefined_values.json b/tests/test_rest_endpoints/services/test_ssh/undefined_values.json deleted file mode 100644 index b7b03d3..0000000 --- a/tests/test_rest_endpoints/services/test_ssh/undefined_values.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": {}, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py deleted file mode 100644 index 4d0d2ed..0000000 --- a/tests/test_rest_endpoints/test_auth.py +++ /dev/null @@ -1,457 +0,0 @@ -# pylint: disable=redefined-outer-name -# pylint: disable=unused-argument -# pylint: disable=missing-function-docstring -import datetime -from datetime import timezone -import pytest - -from tests.conftest import TOKENS_FILE_CONTENTS -from tests.common import ( - RECOVERY_KEY_VALIDATION_DATETIME, - DEVICE_KEY_VALIDATION_DATETIME, - NearFuture, - assert_recovery_recent, -) -from tests.common import five_minutes_into_future_naive_utc as five_minutes_into_future -from tests.common import five_minutes_into_past_naive_utc as five_minutes_into_past - -DATE_FORMATS = [ - "%Y-%m-%dT%H:%M:%S.%fZ", - "%Y-%m-%dT%H:%M:%S.%f", - "%Y-%m-%d %H:%M:%S.%fZ", - "%Y-%m-%d %H:%M:%S.%f", -] - - -def assert_original(client): - new_tokens = rest_get_tokens_info(client) - - for token in TOKENS_FILE_CONTENTS["tokens"]: - assert_token_valid(client, token["token"]) - for new_token in new_tokens: - if new_token["name"] == token["name"]: - assert ( - datetime.datetime.fromisoformat(new_token["date"]) == token["date"] - ) - assert_no_recovery(client) - - -def assert_token_valid(client, token): - client.headers.update({"Authorization": "Bearer " + token}) - assert rest_get_tokens_info(client) is not None - - -def rest_get_tokens_info(client): - response = client.get("/auth/tokens") - assert response.status_code == 200 - return response.json() - - -def rest_try_authorize_new_device(client, token, device_name): - response = client.post( - "/auth/new_device/authorize", - json={ - "token": token, - "device": device_name, - }, - ) - return response - - -def rest_make_recovery_token(client, expires_at=None, timeformat=None, uses=None): - json = {} - - if expires_at is not None: - assert timeformat is not None - expires_at_str = expires_at.strftime(timeformat) - json["expiration"] = expires_at_str - - if uses is not None: - json["uses"] = uses - - if json == {}: - response = client.post("/auth/recovery_token") - else: - response = client.post( - "/auth/recovery_token", - json=json, - ) - - if not response.status_code == 200: - raise ValueError(response.reason, response.text, response.json()["detail"]) - assert response.status_code == 200 - assert "token" in response.json() - return response.json()["token"] - - -def rest_get_recovery_status(client): - response = client.get("/auth/recovery_token") - assert response.status_code == 200 - return response.json() - - -def rest_get_recovery_date(client): - status = rest_get_recovery_status(client) - assert "date" in status - return status["date"] - - -def assert_no_recovery(client): - assert not rest_get_recovery_status(client)["exists"] - - -def rest_recover_with_mnemonic(client, mnemonic_token, device_name): - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": device_name}, - ) - assert recovery_response.status_code == 200 - new_token = recovery_response.json()["token"] - assert_token_valid(client, new_token) - return new_token - - -# Tokens - - -def test_get_tokens_info(authorized_client, tokens_file): - assert sorted(rest_get_tokens_info(authorized_client), key=lambda x: x["name"]) == [ - {"name": "test_token", "date": "2022-01-14T08:31:10.789314", "is_caller": True}, - { - "name": "test_token2", - "date": "2022-01-14T08:31:10.789314", - "is_caller": False, - }, - ] - - -def test_get_tokens_unauthorized(client, tokens_file): - response = client.get("/auth/tokens") - assert response.status_code == 401 - - -def test_delete_token_unauthorized(client, authorized_client, tokens_file): - response = client.delete("/auth/tokens") - assert response.status_code == 401 - assert_original(authorized_client) - - -def test_delete_token(authorized_client, tokens_file): - response = authorized_client.delete( - "/auth/tokens", json={"token_name": "test_token2"} - ) - assert response.status_code == 200 - assert rest_get_tokens_info(authorized_client) == [ - {"name": "test_token", "date": "2022-01-14T08:31:10.789314", "is_caller": True} - ] - - -def test_delete_self_token(authorized_client, tokens_file): - response = authorized_client.delete( - "/auth/tokens", json={"token_name": "test_token"} - ) - assert response.status_code == 400 - assert_original(authorized_client) - - -def test_delete_nonexistent_token(authorized_client, tokens_file): - response = authorized_client.delete( - "/auth/tokens", json={"token_name": "test_token3"} - ) - assert response.status_code == 404 - assert_original(authorized_client) - - -def test_refresh_token_unauthorized(client, authorized_client, tokens_file): - response = client.post("/auth/tokens") - assert response.status_code == 401 - assert_original(authorized_client) - - -def test_refresh_token(authorized_client, tokens_file): - response = authorized_client.post("/auth/tokens") - assert response.status_code == 200 - new_token = response.json()["token"] - assert_token_valid(authorized_client, new_token) - - -# New device - - -def test_get_new_device_auth_token_unauthorized(client, authorized_client, tokens_file): - response = client.post("/auth/new_device") - assert response.status_code == 401 - assert "token" not in response.json() - assert "detail" in response.json() - # We only can check existence of a token we know. - - -def test_get_and_delete_new_device_token(client, authorized_client, tokens_file): - token = rest_get_new_device_token(authorized_client) - response = authorized_client.delete("/auth/new_device", json={"token": token}) - assert response.status_code == 200 - assert rest_try_authorize_new_device(client, token, "new_device").status_code == 404 - - -def test_delete_token_unauthenticated(client, authorized_client, tokens_file): - token = rest_get_new_device_token(authorized_client) - response = client.delete("/auth/new_device", json={"token": token}) - assert response.status_code == 401 - assert rest_try_authorize_new_device(client, token, "new_device").status_code == 200 - - -def rest_get_new_device_token(client): - response = client.post("/auth/new_device") - assert response.status_code == 200 - assert "token" in response.json() - return response.json()["token"] - - -def test_get_and_authorize_new_device(client, authorized_client, tokens_file): - token = rest_get_new_device_token(authorized_client) - response = rest_try_authorize_new_device(client, token, "new_device") - assert response.status_code == 200 - assert_token_valid(authorized_client, response.json()["token"]) - - -def test_authorize_new_device_with_invalid_token( - client, authorized_client, tokens_file -): - response = rest_try_authorize_new_device(client, "invalid_token", "new_device") - assert response.status_code == 404 - assert_original(authorized_client) - - -def test_get_and_authorize_used_token(client, authorized_client, tokens_file): - token_to_be_used_2_times = rest_get_new_device_token(authorized_client) - response = rest_try_authorize_new_device( - client, token_to_be_used_2_times, "new_device" - ) - assert response.status_code == 200 - assert_token_valid(authorized_client, response.json()["token"]) - - response = rest_try_authorize_new_device( - client, token_to_be_used_2_times, "new_device" - ) - assert response.status_code == 404 - - -def test_get_and_authorize_token_after_12_minutes( - client, authorized_client, tokens_file, mocker -): - token = rest_get_new_device_token(authorized_client) - - # TARDIS sounds - mock = mocker.patch(DEVICE_KEY_VALIDATION_DATETIME, NearFuture) - - response = rest_try_authorize_new_device(client, token, "new_device") - assert response.status_code == 404 - assert_original(authorized_client) - - -def test_authorize_without_token(client, authorized_client, tokens_file): - response = client.post( - "/auth/new_device/authorize", - json={"device": "new_device"}, - ) - assert response.status_code == 422 - assert_original(authorized_client) - - -# Recovery tokens -# GET /auth/recovery_token returns token status -# - if token is valid, returns 200 and token status -# - token status: -# - exists (boolean) -# - valid (boolean) -# - date (string) -# - expiration (string) -# - uses_left (int) -# - if token is invalid, returns 400 and empty body -# POST /auth/recovery_token generates a new token -# has two optional parameters: -# - expiration (string in datetime format) -# - uses_left (int) -# POST /auth/recovery_token/use uses the token -# required arguments: -# - token (string) -# - device (string) -# - if token is valid, returns 200 and token -# - if token is invalid, returns 404 -# - if request is invalid, returns 400 - - -def test_get_recovery_token_status_unauthorized(client, authorized_client, tokens_file): - response = client.get("/auth/recovery_token") - assert response.status_code == 401 - assert_original(authorized_client) - - -def test_get_recovery_token_when_none_exists(authorized_client, tokens_file): - response = authorized_client.get("/auth/recovery_token") - assert response.status_code == 200 - assert response.json() == { - "exists": False, - "valid": False, - "date": None, - "expiration": None, - "uses_left": None, - } - assert_original(authorized_client) - - -def test_generate_recovery_token(authorized_client, client, tokens_file): - # Generate token without expiration and uses_left - mnemonic_token = rest_make_recovery_token(authorized_client) - - time_generated = rest_get_recovery_date(authorized_client) - assert_recovery_recent(time_generated) - - assert rest_get_recovery_status(authorized_client) == { - "exists": True, - "valid": True, - "date": time_generated, - "expiration": None, - "uses_left": None, - } - - rest_recover_with_mnemonic(client, mnemonic_token, "recover_device") - # And again - rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2") - - -@pytest.mark.parametrize("timeformat", DATE_FORMATS) -def test_generate_recovery_token_with_expiration_date( - authorized_client, client, tokens_file, timeformat, mocker -): - # Generate token with expiration date - # Generate expiration date in the future - expiration_date = five_minutes_into_future() - mnemonic_token = rest_make_recovery_token( - authorized_client, expires_at=expiration_date, timeformat=timeformat - ) - - time_generated = rest_get_recovery_date(authorized_client) - assert_recovery_recent(time_generated) - - assert rest_get_recovery_status(authorized_client) == { - "exists": True, - "valid": True, - "date": time_generated, - "expiration": expiration_date.replace(tzinfo=timezone.utc).isoformat(), - "uses_left": None, - } - - rest_recover_with_mnemonic(client, mnemonic_token, "recover_device") - # And again - rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2") - - # Try to use token after expiration date - mock = mocker.patch(RECOVERY_KEY_VALIDATION_DATETIME, NearFuture) - device_name = "recovery_device3" - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": device_name}, - ) - assert recovery_response.status_code == 404 - # Assert that the token was not created - assert device_name not in [ - token["name"] for token in rest_get_tokens_info(authorized_client) - ] - - -@pytest.mark.parametrize("timeformat", DATE_FORMATS) -def test_generate_recovery_token_with_expiration_in_the_past( - authorized_client, tokens_file, timeformat -): - # Server must return 400 if expiration date is in the past - expiration_date = five_minutes_into_past() - expiration_date_str = expiration_date.strftime(timeformat) - response = authorized_client.post( - "/auth/recovery_token", - json={"expiration": expiration_date_str}, - ) - assert response.status_code == 400 - assert_no_recovery(authorized_client) - - -def test_generate_recovery_token_with_invalid_time_format( - authorized_client, tokens_file -): - # Server must return 400 if expiration date is in the past - expiration_date = "invalid_time_format" - response = authorized_client.post( - "/auth/recovery_token", - json={"expiration": expiration_date}, - ) - assert response.status_code == 422 - assert_no_recovery(authorized_client) - - -def test_generate_recovery_token_with_limited_uses( - authorized_client, client, tokens_file -): - # Generate token with limited uses - mnemonic_token = rest_make_recovery_token(authorized_client, uses=2) - - time_generated = rest_get_recovery_date(authorized_client) - assert_recovery_recent(time_generated) - - assert rest_get_recovery_status(authorized_client) == { - "exists": True, - "valid": True, - "date": time_generated, - "expiration": None, - "uses_left": 2, - } - - # Try to use the token - rest_recover_with_mnemonic(client, mnemonic_token, "recover_device") - - assert rest_get_recovery_status(authorized_client) == { - "exists": True, - "valid": True, - "date": time_generated, - "expiration": None, - "uses_left": 1, - } - - # Try to use token again - rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2") - - assert rest_get_recovery_status(authorized_client) == { - "exists": True, - "valid": False, - "date": time_generated, - "expiration": None, - "uses_left": 0, - } - - # Try to use token after limited uses - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": "recovery_device3"}, - ) - assert recovery_response.status_code == 404 - - -def test_generate_recovery_token_with_negative_uses( - authorized_client, client, tokens_file -): - # Generate token with limited uses - response = authorized_client.post( - "/auth/recovery_token", - json={"uses": -2}, - ) - assert response.status_code == 400 - assert_no_recovery(authorized_client) - - -def test_generate_recovery_token_with_zero_uses(authorized_client, client, tokens_file): - # Generate token with limited uses - response = authorized_client.post( - "/auth/recovery_token", - json={"uses": 0}, - ) - assert response.status_code == 400 - assert_no_recovery(authorized_client) diff --git a/tests/test_rest_endpoints/test_system.py b/tests/test_rest_endpoints/test_system.py deleted file mode 100644 index 90c1499..0000000 --- a/tests/test_rest_endpoints/test_system.py +++ /dev/null @@ -1,416 +0,0 @@ -# pylint: disable=redefined-outer-name -# pylint: disable=unused-argument -# pylint: disable=missing-function-docstring - -import json -import os -import pytest -from selfprivacy_api.utils import get_domain - - -def read_json(file_path): - with open(file_path, "r", encoding="utf-8") as file: - return json.load(file) - - -@pytest.fixture -def domain_file(mocker, datadir): - mocker.patch("selfprivacy_api.utils.DOMAIN_FILE", datadir / "domain") - return datadir - - -@pytest.fixture -def turned_on(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_on.json") - assert read_json(datadir / "turned_on.json")["autoUpgrade"]["enable"] == True - assert read_json(datadir / "turned_on.json")["autoUpgrade"]["allowReboot"] == True - assert read_json(datadir / "turned_on.json")["timezone"] == "Europe/Moscow" - return datadir - - -@pytest.fixture -def turned_off(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "turned_off.json") - assert read_json(datadir / "turned_off.json")["autoUpgrade"]["enable"] == False - assert read_json(datadir / "turned_off.json")["autoUpgrade"]["allowReboot"] == False - assert read_json(datadir / "turned_off.json")["timezone"] == "Europe/Moscow" - return datadir - - -@pytest.fixture -def undefined_config(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") - assert "autoUpgrade" not in read_json(datadir / "undefined.json") - assert "timezone" not in read_json(datadir / "undefined.json") - return datadir - - -@pytest.fixture -def no_values(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "no_values.json") - assert "enable" not in read_json(datadir / "no_values.json")["autoUpgrade"] - assert "allowReboot" not in read_json(datadir / "no_values.json")["autoUpgrade"] - return datadir - - -class ProcessMock: - """Mock subprocess.Popen""" - - def __init__(self, args, **kwargs): - self.args = args - self.kwargs = kwargs - - def communicate(): - return (b"", None) - - returncode = 0 - - -class BrokenServiceMock(ProcessMock): - """Mock subprocess.Popen""" - - def communicate(): - return (b"Testing error", None) - - returncode = 3 - - -@pytest.fixture -def mock_subprocess_popen(mocker): - mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock) - return mock - - -@pytest.fixture -def mock_os_chdir(mocker): - mock = mocker.patch("os.chdir", autospec=True) - return mock - - -@pytest.fixture -def mock_broken_service(mocker): - mock = mocker.patch( - "subprocess.Popen", autospec=True, return_value=BrokenServiceMock - ) - return mock - - -@pytest.fixture -def mock_subprocess_check_output(mocker): - mock = mocker.patch( - "subprocess.check_output", autospec=True, return_value=b"Testing Linux" - ) - return mock - - -def test_wrong_auth(wrong_auth_client): - response = wrong_auth_client.get("/system/pythonVersion") - assert response.status_code == 401 - - -def test_get_domain(authorized_client, domain_file): - assert get_domain() == "test-domain.tld" - - -## Timezones - - -def test_get_timezone_unauthorized(client, turned_on): - response = client.get("/system/configuration/timezone") - assert response.status_code == 401 - - -def test_get_timezone(authorized_client, turned_on): - response = authorized_client.get("/system/configuration/timezone") - assert response.status_code == 200 - assert response.json() == "Europe/Moscow" - - -def test_get_timezone_on_undefined(authorized_client, undefined_config): - response = authorized_client.get("/system/configuration/timezone") - assert response.status_code == 200 - assert response.json() == "Europe/Uzhgorod" - - -def test_put_timezone_unauthorized(client, turned_on): - response = client.put( - "/system/configuration/timezone", json={"timezone": "Europe/Moscow"} - ) - assert response.status_code == 401 - - -def test_put_timezone(authorized_client, turned_on): - response = authorized_client.put( - "/system/configuration/timezone", json={"timezone": "Europe/Helsinki"} - ) - assert response.status_code == 200 - assert read_json(turned_on / "turned_on.json")["timezone"] == "Europe/Helsinki" - - -def test_put_timezone_on_undefined(authorized_client, undefined_config): - response = authorized_client.put( - "/system/configuration/timezone", json={"timezone": "Europe/Helsinki"} - ) - assert response.status_code == 200 - assert ( - read_json(undefined_config / "undefined.json")["timezone"] == "Europe/Helsinki" - ) - - -def test_put_timezone_without_timezone(authorized_client, turned_on): - response = authorized_client.put("/system/configuration/timezone", json={}) - assert response.status_code == 422 - assert read_json(turned_on / "turned_on.json")["timezone"] == "Europe/Moscow" - - -def test_put_invalid_timezone(authorized_client, turned_on): - response = authorized_client.put( - "/system/configuration/timezone", json={"timezone": "Invalid/Timezone"} - ) - assert response.status_code == 400 - assert read_json(turned_on / "turned_on.json")["timezone"] == "Europe/Moscow" - - -## AutoUpgrade - - -def test_get_auto_upgrade_unauthorized(client, turned_on): - response = client.get("/system/configuration/autoUpgrade") - assert response.status_code == 401 - - -def test_get_auto_upgrade(authorized_client, turned_on): - response = authorized_client.get("/system/configuration/autoUpgrade") - assert response.status_code == 200 - assert response.json() == { - "enable": True, - "allowReboot": True, - } - - -def test_get_auto_upgrade_on_undefined(authorized_client, undefined_config): - response = authorized_client.get("/system/configuration/autoUpgrade") - assert response.status_code == 200 - assert response.json() == { - "enable": True, - "allowReboot": False, - } - - -def test_get_auto_upgrade_without_values(authorized_client, no_values): - response = authorized_client.get("/system/configuration/autoUpgrade") - assert response.status_code == 200 - assert response.json() == { - "enable": True, - "allowReboot": False, - } - - -def test_get_auto_upgrade_turned_off(authorized_client, turned_off): - response = authorized_client.get("/system/configuration/autoUpgrade") - assert response.status_code == 200 - assert response.json() == { - "enable": False, - "allowReboot": False, - } - - -def test_put_auto_upgrade_unauthorized(client, turned_on): - response = client.put( - "/system/configuration/autoUpgrade", json={"enable": True, "allowReboot": True} - ) - assert response.status_code == 401 - - -def test_put_auto_upgrade(authorized_client, turned_on): - response = authorized_client.put( - "/system/configuration/autoUpgrade", json={"enable": False, "allowReboot": True} - ) - assert response.status_code == 200 - assert read_json(turned_on / "turned_on.json")["autoUpgrade"] == { - "enable": False, - "allowReboot": True, - } - - -def test_put_auto_upgrade_on_undefined(authorized_client, undefined_config): - response = authorized_client.put( - "/system/configuration/autoUpgrade", json={"enable": False, "allowReboot": True} - ) - assert response.status_code == 200 - assert read_json(undefined_config / "undefined.json")["autoUpgrade"] == { - "enable": False, - "allowReboot": True, - } - - -def test_put_auto_upgrade_without_values(authorized_client, no_values): - response = authorized_client.put( - "/system/configuration/autoUpgrade", json={"enable": True, "allowReboot": True} - ) - assert response.status_code == 200 - assert read_json(no_values / "no_values.json")["autoUpgrade"] == { - "enable": True, - "allowReboot": True, - } - - -def test_put_auto_upgrade_turned_off(authorized_client, turned_off): - response = authorized_client.put( - "/system/configuration/autoUpgrade", json={"enable": True, "allowReboot": True} - ) - assert response.status_code == 200 - assert read_json(turned_off / "turned_off.json")["autoUpgrade"] == { - "enable": True, - "allowReboot": True, - } - - -def test_put_auto_upgrade_without_enable(authorized_client, turned_off): - response = authorized_client.put( - "/system/configuration/autoUpgrade", json={"allowReboot": True} - ) - assert response.status_code == 200 - assert read_json(turned_off / "turned_off.json")["autoUpgrade"] == { - "enable": False, - "allowReboot": True, - } - - -def test_put_auto_upgrade_without_allow_reboot(authorized_client, turned_off): - response = authorized_client.put( - "/system/configuration/autoUpgrade", json={"enable": True} - ) - assert response.status_code == 200 - assert read_json(turned_off / "turned_off.json")["autoUpgrade"] == { - "enable": True, - "allowReboot": False, - } - - -def test_put_auto_upgrade_with_empty_json(authorized_client, turned_off): - response = authorized_client.put("/system/configuration/autoUpgrade", json={}) - assert response.status_code == 200 - assert read_json(turned_off / "turned_off.json")["autoUpgrade"] == { - "enable": False, - "allowReboot": False, - } - - -def test_system_rebuild_unauthorized(client, mock_subprocess_popen): - response = client.get("/system/configuration/apply") - assert response.status_code == 401 - assert mock_subprocess_popen.call_count == 0 - - -def test_system_rebuild(authorized_client, mock_subprocess_popen): - response = authorized_client.get("/system/configuration/apply") - assert response.status_code == 200 - assert mock_subprocess_popen.call_count == 1 - assert mock_subprocess_popen.call_args[0][0] == [ - "systemctl", - "start", - "sp-nixos-rebuild.service", - ] - - -def test_system_upgrade_unauthorized(client, mock_subprocess_popen): - response = client.get("/system/configuration/upgrade") - assert response.status_code == 401 - assert mock_subprocess_popen.call_count == 0 - - -def test_system_upgrade(authorized_client, mock_subprocess_popen): - response = authorized_client.get("/system/configuration/upgrade") - assert response.status_code == 200 - assert mock_subprocess_popen.call_count == 1 - assert mock_subprocess_popen.call_args[0][0] == [ - "systemctl", - "start", - "sp-nixos-upgrade.service", - ] - - -def test_system_rollback_unauthorized(client, mock_subprocess_popen): - response = client.get("/system/configuration/rollback") - assert response.status_code == 401 - assert mock_subprocess_popen.call_count == 0 - - -def test_system_rollback(authorized_client, mock_subprocess_popen): - response = authorized_client.get("/system/configuration/rollback") - assert response.status_code == 200 - assert mock_subprocess_popen.call_count == 1 - assert mock_subprocess_popen.call_args[0][0] == [ - "systemctl", - "start", - "sp-nixos-rollback.service", - ] - - -def test_get_system_version_unauthorized(client, mock_subprocess_check_output): - response = client.get("/system/version") - assert response.status_code == 401 - assert mock_subprocess_check_output.call_count == 0 - - -def test_get_system_version(authorized_client, mock_subprocess_check_output): - response = authorized_client.get("/system/version") - assert response.status_code == 200 - assert response.json() == {"system_version": "Testing Linux"} - assert mock_subprocess_check_output.call_count == 1 - assert mock_subprocess_check_output.call_args[0][0] == ["uname", "-a"] - - -def test_reboot_system_unauthorized(client, mock_subprocess_popen): - response = client.get("/system/reboot") - assert response.status_code == 401 - assert mock_subprocess_popen.call_count == 0 - - -def test_reboot_system(authorized_client, mock_subprocess_popen): - response = authorized_client.get("/system/reboot") - assert response.status_code == 200 - assert mock_subprocess_popen.call_count == 1 - assert mock_subprocess_popen.call_args[0][0] == ["reboot"] - - -def test_get_python_version_unauthorized(client, mock_subprocess_check_output): - response = client.get("/system/pythonVersion") - assert response.status_code == 401 - assert mock_subprocess_check_output.call_count == 0 - - -def test_get_python_version(authorized_client, mock_subprocess_check_output): - response = authorized_client.get("/system/pythonVersion") - assert response.status_code == 200 - assert response.json() == "Testing Linux" - assert mock_subprocess_check_output.call_count == 1 - assert mock_subprocess_check_output.call_args[0][0] == ["python", "-V"] - - -def test_pull_system_unauthorized(client, mock_subprocess_popen): - response = client.get("/system/configuration/pull") - assert response.status_code == 401 - assert mock_subprocess_popen.call_count == 0 - - -def test_pull_system(authorized_client, mock_subprocess_popen, mock_os_chdir): - current_dir = os.getcwd() - response = authorized_client.get("/system/configuration/pull") - assert response.status_code == 200 - assert mock_subprocess_popen.call_count == 1 - assert mock_subprocess_popen.call_args[0][0] == ["git", "pull"] - assert mock_os_chdir.call_count == 2 - assert mock_os_chdir.call_args_list[0][0][0] == "/etc/nixos" - assert mock_os_chdir.call_args_list[1][0][0] == current_dir - - -def test_pull_system_broken_repo(authorized_client, mock_broken_service, mock_os_chdir): - current_dir = os.getcwd() - response = authorized_client.get("/system/configuration/pull") - assert response.status_code == 500 - assert mock_broken_service.call_count == 1 - assert mock_os_chdir.call_count == 2 - assert mock_os_chdir.call_args_list[0][0][0] == "/etc/nixos" - assert mock_os_chdir.call_args_list[1][0][0] == current_dir diff --git a/tests/test_rest_endpoints/test_system/no_values.json b/tests/test_rest_endpoints/test_system/no_values.json deleted file mode 100644 index 5c1431e..0000000 --- a/tests/test_rest_endpoints/test_system/no_values.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": true - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/test_system/turned_off.json b/tests/test_rest_endpoints/test_system/turned_off.json deleted file mode 100644 index 2336f36..0000000 --- a/tests/test_rest_endpoints/test_system/turned_off.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": true - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": false, - "allowReboot": false - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/test_system/turned_on.json b/tests/test_rest_endpoints/test_system/turned_on.json deleted file mode 100644 index 42999d8..0000000 --- a/tests/test_rest_endpoints/test_system/turned_on.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": true - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/test_system/undefined.json b/tests/test_rest_endpoints/test_system/undefined.json deleted file mode 100644 index 6b9f3fd..0000000 --- a/tests/test_rest_endpoints/test_system/undefined.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": true - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/test_users.py b/tests/test_rest_endpoints/test_users.py deleted file mode 100644 index ebb3eff..0000000 --- a/tests/test_rest_endpoints/test_users.py +++ /dev/null @@ -1,285 +0,0 @@ -# pylint: disable=redefined-outer-name -# pylint: disable=unused-argument -import json -import pytest - - -def read_json(file_path): - with open(file_path, "r", encoding="utf-8") as file: - return json.load(file) - - -invalid_usernames = [ - "root", - "messagebus", - "postfix", - "polkituser", - "dovecot2", - "dovenull", - "nginx", - "postgres", - "systemd-journal-gateway", - "prosody", - "systemd-network", - "systemd-resolve", - "systemd-timesync", - "opendkim", - "rspamd", - "sshd", - "selfprivacy-api", - "restic", - "redis", - "pleroma", - "ocserv", - "nextcloud", - "memcached", - "knot-resolver", - "gitea", - "bitwarden_rs", - "vaultwarden", - "acme", - "virtualMail", - "nixbld1", - "nixbld2", - "nixbld29", - "nobody", -] - - -## FIXTURES ################################################### - - -@pytest.fixture -def no_users(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "no_users.json") - assert read_json(datadir / "no_users.json")["users"] == [] - return datadir - - -@pytest.fixture -def one_user(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "one_user.json") - assert read_json(datadir / "one_user.json")["users"] == [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": ["ssh-rsa KEY user1@pc"], - } - ] - return datadir - - -@pytest.fixture -def some_users(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "some_users.json") - assert read_json(datadir / "some_users.json")["users"] == [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": ["ssh-rsa KEY user1@pc"], - }, - {"username": "user2", "hashedPassword": "HASHED_PASSWORD_2", "sshKeys": []}, - {"username": "user3", "hashedPassword": "HASHED_PASSWORD_3"}, - ] - return datadir - - -@pytest.fixture -def undefined_settings(mocker, datadir): - mocker.patch("selfprivacy_api.utils.USERDATA_FILE", new=datadir / "undefined.json") - assert "users" not in read_json(datadir / "undefined.json") - return datadir - - -class ProcessMock: - """Mock subprocess.Popen""" - - def __init__(self, args, **kwargs): - self.args = args - self.kwargs = kwargs - - def communicate(): - return (b"NEW_HASHED", None) - - returncode = 0 - - -@pytest.fixture -def mock_subprocess_popen(mocker): - mock = mocker.patch("subprocess.Popen", autospec=True, return_value=ProcessMock) - return mock - - -## TESTS ###################################################### - - -def test_get_users_unauthorized(client, some_users, mock_subprocess_popen): - response = client.get("/users") - assert response.status_code == 401 - - -def test_get_some_users(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.get("/users") - assert response.status_code == 200 - assert response.json() == ["user1", "user2", "user3"] - - -def test_get_one_user(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.get("/users") - assert response.status_code == 200 - assert response.json() == ["user1"] - - -def test_get_one_user_with_main(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.get("/users?withMainUser=true") - assert response.status_code == 200 - assert response.json().sort() == ["tester", "user1"].sort() - - -def test_get_no_users(authorized_client, no_users, mock_subprocess_popen): - response = authorized_client.get("/users") - assert response.status_code == 200 - assert response.json() == [] - - -def test_get_no_users_with_main(authorized_client, no_users, mock_subprocess_popen): - response = authorized_client.get("/users?withMainUser=true") - assert response.status_code == 200 - assert response.json() == ["tester"] - - -def test_get_undefined_users( - authorized_client, undefined_settings, mock_subprocess_popen -): - response = authorized_client.get("/users") - assert response.status_code == 200 - assert response.json() == [] - - -def test_post_users_unauthorized(client, some_users, mock_subprocess_popen): - response = client.post("/users") - assert response.status_code == 401 - - -def test_post_one_user(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post( - "/users", json={"username": "user4", "password": "password"} - ) - assert response.status_code == 201 - assert read_json(one_user / "one_user.json")["users"] == [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": ["ssh-rsa KEY user1@pc"], - }, - { - "username": "user4", - "sshKeys": [], - "hashedPassword": "NEW_HASHED", - }, - ] - - -def test_post_without_username(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post("/users", json={"password": "password"}) - assert response.status_code == 422 - - -def test_post_without_password(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post("/users", json={"username": "user4"}) - assert response.status_code == 422 - - -def test_post_without_username_and_password( - authorized_client, one_user, mock_subprocess_popen -): - response = authorized_client.post("/users", json={}) - assert response.status_code == 422 - - -@pytest.mark.parametrize("username", invalid_usernames) -def test_post_system_user(authorized_client, one_user, mock_subprocess_popen, username): - response = authorized_client.post( - "/users", json={"username": username, "password": "password"} - ) - assert response.status_code == 409 - - -def test_post_existing_user(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post( - "/users", json={"username": "user1", "password": "password"} - ) - assert response.status_code == 409 - - -def test_post_existing_main_user(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post( - "/users", json={"username": "tester", "password": "password"} - ) - assert response.status_code == 409 - - -def test_post_user_to_undefined_users( - authorized_client, undefined_settings, mock_subprocess_popen -): - response = authorized_client.post( - "/users", json={"username": "user4", "password": "password"} - ) - assert response.status_code == 201 - assert read_json(undefined_settings / "undefined.json")["users"] == [ - {"username": "user4", "sshKeys": [], "hashedPassword": "NEW_HASHED"} - ] - - -def test_post_very_long_username(authorized_client, one_user, mock_subprocess_popen): - response = authorized_client.post( - "/users", json={"username": "a" * 32, "password": "password"} - ) - assert response.status_code == 400 - - -@pytest.mark.parametrize("username", ["", "1", "фыр", "user1@", "№:%##$^&@$&^()_"]) -def test_post_invalid_username( - authorized_client, one_user, mock_subprocess_popen, username -): - response = authorized_client.post( - "/users", json={"username": username, "password": "password"} - ) - assert response.status_code == 400 - - -def test_delete_user_unauthorized(client, some_users, mock_subprocess_popen): - response = client.delete("/users/user1") - assert response.status_code == 401 - - -def test_delete_user_not_found(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.delete("/users/user4") - assert response.status_code == 404 - - -def test_delete_user(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.delete("/users/user1") - assert response.status_code == 200 - assert read_json(some_users / "some_users.json")["users"] == [ - {"username": "user2", "hashedPassword": "HASHED_PASSWORD_2", "sshKeys": []}, - {"username": "user3", "hashedPassword": "HASHED_PASSWORD_3"}, - ] - - -@pytest.mark.parametrize("username", invalid_usernames) -def test_delete_system_user( - authorized_client, some_users, mock_subprocess_popen, username -): - response = authorized_client.delete("/users/" + username) - assert response.status_code == 400 or response.status_code == 404 - - -def test_delete_main_user(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.delete("/users/tester") - assert response.status_code == 400 - - -def test_delete_just_delete(authorized_client, some_users, mock_subprocess_popen): - response = authorized_client.delete("/users") - assert response.status_code == 405 diff --git a/tests/test_rest_endpoints/test_users/no_users.json b/tests/test_rest_endpoints/test_users/no_users.json deleted file mode 100644 index 5929a79..0000000 --- a/tests/test_rest_endpoints/test_users/no_users.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "users": [ - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/test_users/one_user.json b/tests/test_rest_endpoints/test_users/one_user.json deleted file mode 100644 index 6c553bc..0000000 --- a/tests/test_rest_endpoints/test_users/one_user.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "users": [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": [ - "ssh-rsa KEY user1@pc" - ] - } - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/test_users/some_users.json b/tests/test_rest_endpoints/test_users/some_users.json deleted file mode 100644 index df6380a..0000000 --- a/tests/test_rest_endpoints/test_users/some_users.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "users": [ - { - "username": "user1", - "hashedPassword": "HASHED_PASSWORD_1", - "sshKeys": [ - "ssh-rsa KEY user1@pc" - ] - }, - { - "username": "user2", - "hashedPassword": "HASHED_PASSWORD_2", - "sshKeys": [ - ] - }, - { - "username": "user3", - "hashedPassword": "HASHED_PASSWORD_3" - } - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/test_users/undefined.json b/tests/test_rest_endpoints/test_users/undefined.json deleted file mode 100644 index c1691ea..0000000 --- a/tests/test_rest_endpoints/test_users/undefined.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "api": { - "token": "TEST_TOKEN", - "enableSwagger": false - }, - "bitwarden": { - "enable": false - }, - "databasePassword": "PASSWORD", - "domain": "test.tld", - "hashedMasterPassword": "HASHED_PASSWORD", - "hostname": "test-instance", - "nextcloud": { - "adminPassword": "ADMIN", - "databasePassword": "ADMIN", - "enable": true - }, - "resticPassword": "PASS", - "ssh": { - "enable": true, - "passwordAuthentication": true, - "rootKeys": [ - "ssh-ed25519 KEY test@pc" - ] - }, - "username": "tester", - "gitea": { - "enable": false - }, - "ocserv": { - "enable": true - }, - "pleroma": { - "enable": true - }, - "autoUpgrade": { - "enable": true, - "allowReboot": true - }, - "timezone": "Europe/Moscow", - "sshKeys": [ - "ssh-rsa KEY test@pc" - ], - "dns": { - "provider": "CLOUDFLARE", - "apiKey": "TOKEN" - }, - "server": { - "provider": "HETZNER" - }, - "backup": { - "provider": "BACKBLAZE", - "accountId": "ID", - "accountKey": "KEY", - "bucket": "selfprivacy" - } -} \ No newline at end of file diff --git a/tests/test_services.py b/tests/test_services.py index b83a7f2..65b4dc9 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -1,18 +1,25 @@ """ Tests for generic service methods """ +import pytest from pytest import raises +from selfprivacy_api.utils import ReadUserData, WriteUserData +from selfprivacy_api.utils.waitloop import wait_until_true + +import selfprivacy_api.services as services_module + from selfprivacy_api.services.bitwarden import Bitwarden from selfprivacy_api.services.pleroma import Pleroma +from selfprivacy_api.services.mailserver import MailServer from selfprivacy_api.services.owned_path import OwnedPath from selfprivacy_api.services.generic_service_mover import FolderMoveNames from selfprivacy_api.services.test_service import DummyService from selfprivacy_api.services.service import Service, ServiceStatus, StoppedService -from selfprivacy_api.utils.waitloop import wait_until_true +from selfprivacy_api.services import get_enabled_services -from tests.test_graphql.test_backup import raw_dummy_service +from tests.test_dkim import domain_file, dkim_file, no_dkim_file def test_unimplemented_folders_raises(): @@ -87,3 +94,88 @@ def test_foldermoves_from_ownedpaths(): group="vaultwarden", owner="vaultwarden", ) + + +def test_enabling_disabling_reads_json(dummy_service: DummyService): + with WriteUserData() as data: + data["modules"][dummy_service.get_id()]["enable"] = False + assert dummy_service.is_enabled() is False + with WriteUserData() as data: + data["modules"][dummy_service.get_id()]["enable"] = True + assert dummy_service.is_enabled() is True + + +# A helper to test undefined states. Used in fixtures below +def undefine_service_enabled_status(param, dummy_service): + if param == "deleted_attribute": + with WriteUserData() as data: + del data["modules"][dummy_service.get_id()]["enable"] + if param == "service_not_in_json": + with WriteUserData() as data: + del data["modules"][dummy_service.get_id()] + if param == "modules_not_in_json": + with WriteUserData() as data: + del data["modules"] + + +# May be defined or not +@pytest.fixture( + params=[ + "normally_enabled", + "deleted_attribute", + "service_not_in_json", + "modules_not_in_json", + ] +) +def possibly_dubiously_enabled_service( + dummy_service: DummyService, request +) -> DummyService: + if request.param != "normally_enabled": + undefine_service_enabled_status(request.param, dummy_service) + return dummy_service + + +# Strictly UNdefined +@pytest.fixture( + params=["deleted_attribute", "service_not_in_json", "modules_not_in_json"] +) +def undefined_enabledness_service(dummy_service: DummyService, request) -> DummyService: + undefine_service_enabled_status(request.param, dummy_service) + return dummy_service + + +def test_undefined_enabledness_in_json_means_False( + undefined_enabledness_service: DummyService, +): + dummy_service = undefined_enabledness_service + assert dummy_service.is_enabled() is False + + +def test_enabling_disabling_writes_json( + possibly_dubiously_enabled_service: DummyService, +): + dummy_service = possibly_dubiously_enabled_service + + dummy_service.disable() + with ReadUserData() as data: + assert data["modules"][dummy_service.get_id()]["enable"] is False + dummy_service.enable() + with ReadUserData() as data: + assert data["modules"][dummy_service.get_id()]["enable"] is True + dummy_service.disable() + with ReadUserData() as data: + assert data["modules"][dummy_service.get_id()]["enable"] is False + + +# more detailed testing of this is in test_graphql/test_system.py +def test_mailserver_with_dkim_returns_some_dns(dkim_file): + records = MailServer().get_dns_records() + assert len(records) > 0 + + +def test_mailserver_with_no_dkim_returns_no_dns(no_dkim_file): + assert MailServer().get_dns_records() == [] + + +def test_services_enabled_by_default(generic_userdata): + assert set(get_enabled_services()) == set(services_module.services) diff --git a/tests/test_services_systemctl.py b/tests/test_services_systemctl.py new file mode 100644 index 0000000..8b247e0 --- /dev/null +++ b/tests/test_services_systemctl.py @@ -0,0 +1,94 @@ +import pytest + +from selfprivacy_api.services.service import ServiceStatus +from selfprivacy_api.services.bitwarden import Bitwarden +from selfprivacy_api.services.gitea import Gitea +from selfprivacy_api.services.mailserver import MailServer +from selfprivacy_api.services.nextcloud import Nextcloud +from selfprivacy_api.services.ocserv import Ocserv +from selfprivacy_api.services.pleroma import Pleroma + + +def expected_status_call(service_name: str): + return ["systemctl", "show", service_name] + + +def call_args_asserts(mocked_object): + assert mocked_object.call_count == 7 + calls = [callargs[0][0] for callargs in mocked_object.call_args_list] + assert calls == [ + expected_status_call(service) + for service in [ + "dovecot2.service", + "postfix.service", + "vaultwarden.service", + "gitea.service", + "phpfpm-nextcloud.service", + "ocserv.service", + "pleroma.service", + ] + ] + + +SUCCESSFUL_STATUS = b""" +Type=oneshot +ExitType=main +Restart=no +NotifyAccess=none +RestartUSec=100ms +LoadState=loaded +ActiveState=active +FreezerState=running +SubState=exited +""" + +FAILED_STATUS = b""" +Type=oneshot +ExitType=main +Restart=no +NotifyAccess=none +RestartUSec=100ms +LoadState=loaded +ActiveState=failed +FreezerState=running +SubState=exited +""" + + +@pytest.fixture +def mock_popen_systemctl_service_ok(mocker): + mock = mocker.patch( + "subprocess.check_output", autospec=True, return_value=SUCCESSFUL_STATUS + ) + return mock + + +@pytest.fixture +def mock_popen_systemctl_service_not_ok(mocker): + mock = mocker.patch( + "subprocess.check_output", autospec=True, return_value=FAILED_STATUS + ) + return mock + + +############################################################################### + + +def test_systemctl_ok(mock_popen_systemctl_service_ok): + assert MailServer.get_status() == ServiceStatus.ACTIVE + assert Bitwarden.get_status() == ServiceStatus.ACTIVE + assert Gitea.get_status() == ServiceStatus.ACTIVE + assert Nextcloud.get_status() == ServiceStatus.ACTIVE + assert Ocserv.get_status() == ServiceStatus.ACTIVE + assert Pleroma.get_status() == ServiceStatus.ACTIVE + call_args_asserts(mock_popen_systemctl_service_ok) + + +def test_systemctl_failed_service(mock_popen_systemctl_service_not_ok): + assert MailServer.get_status() == ServiceStatus.FAILED + assert Bitwarden.get_status() == ServiceStatus.FAILED + assert Gitea.get_status() == ServiceStatus.FAILED + assert Nextcloud.get_status() == ServiceStatus.FAILED + assert Ocserv.get_status() == ServiceStatus.FAILED + assert Pleroma.get_status() == ServiceStatus.FAILED + call_args_asserts(mock_popen_systemctl_service_not_ok) diff --git a/tests/test_ssh.py b/tests/test_ssh.py new file mode 100644 index 0000000..a688a63 --- /dev/null +++ b/tests/test_ssh.py @@ -0,0 +1,432 @@ +""" +Action-level tests of ssh +(For API-independent logic incl. connection to persistent storage) +""" + +import pytest +from typing import Optional + +from selfprivacy_api.actions.ssh import ( + set_ssh_settings, + get_ssh_settings, + create_ssh_key, + remove_ssh_key, + KeyNotFound, + UserNotFound, +) +from selfprivacy_api.actions.users import ( + get_users, + get_user_by_username, + UserDataUserOrigin, +) +from selfprivacy_api.utils import WriteUserData, ReadUserData + + +@pytest.fixture(params=[True, False]) +def bool_value(request): + return request.param + + +@pytest.fixture( + params=[ + "normal_populated_json", + "deleted_enabled", + "deleted_auth", + "empty", + "ssh_not_in_json", + ] +) +def possibly_undefined_ssh_settings(generic_userdata, request, bool_value): + with WriteUserData() as data: + data["ssh"] = {"enable": bool_value, "passswordAuthentication": bool_value} + assert get_raw_json_ssh_setting("enable") == bool_value + assert get_raw_json_ssh_setting("passswordAuthentication") == bool_value + + if request.param == "deleted_enabled": + with WriteUserData() as data: + del data["ssh"]["enable"] + + if request.param == "deleted_auth": + with WriteUserData() as data: + del data["ssh"]["passswordAuthentication"] + + if request.param == "empty": + with WriteUserData() as data: + del data["ssh"]["passswordAuthentication"] + del data["ssh"]["enable"] + + if request.param == "ssh_not_in_json": + with WriteUserData() as data: + del data["ssh"] + + +@pytest.fixture(params=[True, False, None]) +def ssh_enable_spectrum(request): + return request.param + + +@pytest.fixture(params=[True, False, None]) +def password_auth_spectrum(request): + return request.param + + +def admin_name() -> Optional[str]: + users = get_users() + for user in users: + if user.origin == UserDataUserOrigin.PRIMARY: + return user.username + return None + + +def get_raw_json_ssh_setting(setting: str): + with ReadUserData() as data: + return (data.get("ssh") or {}).get(setting) + + +def test_read_json(possibly_undefined_ssh_settings): + with ReadUserData() as data: + if "ssh" not in data.keys(): + assert get_ssh_settings().enable is not None + assert get_ssh_settings().passwordAuthentication is not None + + # TODO: Is it really a good idea to have password ssh enabled by default? + assert get_ssh_settings().enable is True + assert get_ssh_settings().passwordAuthentication is True + return + + if "enable" not in data["ssh"].keys(): + assert get_ssh_settings().enable is True + else: + assert get_ssh_settings().enable == data["ssh"]["enable"] + + if "passwordAuthentication" not in data["ssh"].keys(): + assert get_ssh_settings().passwordAuthentication is True + else: + assert ( + get_ssh_settings().passwordAuthentication + == data["ssh"]["passwordAuthentication"] + ) + + +def test_enabling_disabling_writes_json( + possibly_undefined_ssh_settings, ssh_enable_spectrum, password_auth_spectrum +): + + original_enable = get_raw_json_ssh_setting("enable") + original_password_auth = get_raw_json_ssh_setting("passwordAuthentication") + + set_ssh_settings(ssh_enable_spectrum, password_auth_spectrum) + + with ReadUserData() as data: + if ssh_enable_spectrum is None: + assert get_raw_json_ssh_setting("enable") == original_enable + else: + assert get_raw_json_ssh_setting("enable") == ssh_enable_spectrum + + if password_auth_spectrum is None: + assert ( + get_raw_json_ssh_setting("passwordAuthentication") + == original_password_auth + ) + else: + assert ( + get_raw_json_ssh_setting("passwordAuthentication") + == password_auth_spectrum + ) + + +############### ROOTKEYS + + +def test_read_root_keys_from_json(generic_userdata): + assert get_ssh_settings().rootKeys == ["ssh-ed25519 KEY test@pc"] + new_keys = ["ssh-ed25519 KEY test@pc", "ssh-ed25519 KEY2 test@pc"] + + with WriteUserData() as data: + data["ssh"]["rootKeys"] = new_keys + + assert get_ssh_settings().rootKeys == new_keys + + with WriteUserData() as data: + del data["ssh"]["rootKeys"] + + assert get_ssh_settings().rootKeys == [] + + with WriteUserData() as data: + del data["ssh"] + + assert get_ssh_settings().rootKeys == [] + + +def test_removing_root_key_writes_json(generic_userdata): + # generic userdata has a a single root key + rootkeys = get_ssh_settings().rootKeys + assert len(rootkeys) == 1 + key1 = rootkeys[0] + key2 = "ssh-rsa MYSUPERKEY root@pc" + + create_ssh_key("root", key2) + rootkeys = get_ssh_settings().rootKeys + assert len(rootkeys) == 2 + + remove_ssh_key("root", key2) + with ReadUserData() as data: + assert "ssh" in data + assert "rootKeys" in data["ssh"] + assert data["ssh"]["rootKeys"] == [key1] + + remove_ssh_key("root", key1) + with ReadUserData() as data: + assert "ssh" in data + assert "rootKeys" in data["ssh"] + assert data["ssh"]["rootKeys"] == [] + + +def test_remove_root_key_on_undefined(generic_userdata): + # generic userdata has a a single root key + rootkeys = get_ssh_settings().rootKeys + assert len(rootkeys) == 1 + key1 = rootkeys[0] + + with WriteUserData() as data: + del data["ssh"]["rootKeys"] + + with pytest.raises(KeyNotFound): + remove_ssh_key("root", key1) + rootkeys = get_ssh_settings().rootKeys + assert len(rootkeys) == 0 + + with WriteUserData() as data: + del data["ssh"] + + with pytest.raises(KeyNotFound): + remove_ssh_key("root", key1) + rootkeys = get_ssh_settings().rootKeys + assert len(rootkeys) == 0 + + +def test_adding_root_key_writes_json(generic_userdata): + with WriteUserData() as data: + del data["ssh"] + key1 = "ssh-ed25519 KEY test@pc" + key2 = "ssh-ed25519 KEY2 test@pc" + create_ssh_key("root", key1) + + with ReadUserData() as data: + assert "ssh" in data + assert "rootKeys" in data["ssh"] + assert data["ssh"]["rootKeys"] == [key1] + + with WriteUserData() as data: + del data["ssh"]["rootKeys"] + create_ssh_key("root", key1) + + with ReadUserData() as data: + assert "ssh" in data + assert "rootKeys" in data["ssh"] + assert data["ssh"]["rootKeys"] == [key1] + + create_ssh_key("root", key2) + + with ReadUserData() as data: + assert "ssh" in data + assert "rootKeys" in data["ssh"] + # order is irrelevant + assert set(data["ssh"]["rootKeys"]) == set([key1, key2]) + + +############### ADMIN KEYS + + +def test_read_admin_keys_from_json(generic_userdata): + admin_name = "tester" + assert get_user_by_username(admin_name).ssh_keys == ["ssh-rsa KEY test@pc"] + new_keys = ["ssh-rsa KEY test@pc", "ssh-ed25519 KEY2 test@pc"] + + with WriteUserData() as data: + data["sshKeys"] = new_keys + + assert get_user_by_username(admin_name).ssh_keys == new_keys + + with WriteUserData() as data: + del data["sshKeys"] + + assert get_user_by_username(admin_name).ssh_keys == [] + + +def test_adding_admin_key_writes_json(generic_userdata): + admin_name = "tester" + + with WriteUserData() as data: + del data["sshKeys"] + key1 = "ssh-ed25519 KEY test@pc" + key2 = "ssh-ed25519 KEY2 test@pc" + create_ssh_key(admin_name, key1) + + with ReadUserData() as data: + assert "sshKeys" in data + assert data["sshKeys"] == [key1] + + create_ssh_key(admin_name, key2) + + with ReadUserData() as data: + assert "sshKeys" in data + # order is irrelevant + assert set(data["sshKeys"]) == set([key1, key2]) + + +def test_removing_admin_key_writes_json(generic_userdata): + # generic userdata has a a single admin key + admin_name = "tester" + + admin_keys = get_user_by_username(admin_name).ssh_keys + assert len(admin_keys) == 1 + key1 = admin_keys[0] + key2 = "ssh-rsa MYSUPERKEY admin@pc" + + create_ssh_key(admin_name, key2) + admin_keys = get_user_by_username(admin_name).ssh_keys + assert len(admin_keys) == 2 + + remove_ssh_key(admin_name, key2) + + with ReadUserData() as data: + assert "sshKeys" in data + assert data["sshKeys"] == [key1] + + remove_ssh_key(admin_name, key1) + with ReadUserData() as data: + assert "sshKeys" in data + assert data["sshKeys"] == [] + + +def test_remove_admin_key_on_undefined(generic_userdata): + # generic userdata has a a single admin key + admin_name = "tester" + + admin_keys = get_user_by_username(admin_name).ssh_keys + assert len(admin_keys) == 1 + key1 = admin_keys[0] + + with WriteUserData() as data: + del data["sshKeys"] + + with pytest.raises(KeyNotFound): + remove_ssh_key(admin_name, key1) + admin_keys = get_user_by_username(admin_name).ssh_keys + assert len(admin_keys) == 0 + + +############### USER KEYS + +regular_users = ["user1", "user2", "user3"] + + +def find_user_index_in_json_users(users: list, username: str) -> Optional[int]: + for i, user in enumerate(users): + if user["username"] == username: + return i + return None + + +@pytest.mark.parametrize("username", regular_users) +def test_read_user_keys_from_json(generic_userdata, username): + old_keys = [f"ssh-rsa KEY {username}@pc"] + assert get_user_by_username(username).ssh_keys == old_keys + new_keys = ["ssh-rsa KEY test@pc", "ssh-ed25519 KEY2 test@pc"] + + with WriteUserData() as data: + user_index = find_user_index_in_json_users(data["users"], username) + data["users"][user_index]["sshKeys"] = new_keys + + assert get_user_by_username(username).ssh_keys == new_keys + + with WriteUserData() as data: + user_index = find_user_index_in_json_users(data["users"], username) + del data["users"][user_index]["sshKeys"] + + assert get_user_by_username(username).ssh_keys == [] + + # deeper deletions are for user getter tests, not here + + +@pytest.mark.parametrize("username", regular_users) +def test_adding_user_key_writes_json(generic_userdata, username): + + with WriteUserData() as data: + user_index = find_user_index_in_json_users(data["users"], username) + del data["users"][user_index]["sshKeys"] + key1 = "ssh-ed25519 KEY test@pc" + key2 = "ssh-ed25519 KEY2 test@pc" + create_ssh_key(username, key1) + + with ReadUserData() as data: + user_index = find_user_index_in_json_users(data["users"], username) + assert "sshKeys" in data["users"][user_index] + assert data["users"][user_index]["sshKeys"] == [key1] + + create_ssh_key(username, key2) + + with ReadUserData() as data: + user_index = find_user_index_in_json_users(data["users"], username) + assert "sshKeys" in data["users"][user_index] + # order is irrelevant + assert set(data["users"][user_index]["sshKeys"]) == set([key1, key2]) + + +@pytest.mark.parametrize("username", regular_users) +def test_removing_user_key_writes_json(generic_userdata, username): + # generic userdata has a a single user key + + user_keys = get_user_by_username(username).ssh_keys + assert len(user_keys) == 1 + key1 = user_keys[0] + key2 = "ssh-rsa MYSUPERKEY admin@pc" + + create_ssh_key(username, key2) + user_keys = get_user_by_username(username).ssh_keys + assert len(user_keys) == 2 + + remove_ssh_key(username, key2) + + with ReadUserData() as data: + user_index = find_user_index_in_json_users(data["users"], username) + assert "sshKeys" in data["users"][user_index] + assert data["users"][user_index]["sshKeys"] == [key1] + + remove_ssh_key(username, key1) + with ReadUserData() as data: + user_index = find_user_index_in_json_users(data["users"], username) + assert "sshKeys" in data["users"][user_index] + assert data["users"][user_index]["sshKeys"] == [] + + +@pytest.mark.parametrize("username", regular_users) +def test_remove_user_key_on_undefined(generic_userdata, username): + # generic userdata has a a single user key + user_keys = get_user_by_username(username).ssh_keys + assert len(user_keys) == 1 + key1 = user_keys[0] + + with WriteUserData() as data: + user_index = find_user_index_in_json_users(data["users"], username) + del data["users"][user_index]["sshKeys"] + + with pytest.raises(KeyNotFound): + remove_ssh_key(username, key1) + + user_keys = get_user_by_username(username).ssh_keys + assert len(user_keys) == 0 + + with WriteUserData() as data: + user_index = find_user_index_in_json_users(data["users"], username) + del data["users"][user_index] + + with pytest.raises(UserNotFound): + remove_ssh_key(username, key1) + + with WriteUserData() as data: + del data["users"] + + with pytest.raises(UserNotFound): + remove_ssh_key(username, key1) diff --git a/tests/test_system.py b/tests/test_system.py new file mode 100644 index 0000000..549692e --- /dev/null +++ b/tests/test_system.py @@ -0,0 +1,22 @@ +import pytest +from selfprivacy_api.actions.system import run_blocking, ShellException + +# uname is just an arbitrary command expected to be everywhere we care + + +def test_uname(): + output = run_blocking(["uname"]) + assert output is not None + + +def test_uname_new_session(): + output = run_blocking(["uname"], new_session=True) + assert output is not None + + +def test_uname_nonexistent_args(): + with pytest.raises(ShellException) as exception_info: + # uname: extra operand ‘sldfkjsljf’ + # Try 'uname --help' for more information + run_blocking(["uname", "isdyfhishfaisljhkeysmash"], new_session=True) + assert "extra operand" in exception_info.value.args[0] diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..2f613db --- /dev/null +++ b/tests/test_users.py @@ -0,0 +1,26 @@ +from selfprivacy_api.utils import ReadUserData, WriteUserData +from selfprivacy_api.actions.users import delete_user +""" + A place for user storage tests and other user tests that are not Graphql-specific. +""" + +# yes it is an incomplete suite. +# It was born in order to not lose things that REST API tests checked for +# In the future, user storage tests that are not dependent on actual API (graphql or otherwise) go here. + +def test_delete_user_writes_json(generic_userdata): + delete_user("user2") + with ReadUserData() as data: + assert data["users"] == [ + { + "username": "user1", + "hashedPassword": "HASHED_PASSWORD_1", + "sshKeys": ["ssh-rsa KEY user1@pc"] + }, + { + "username": "user3", + "hashedPassword": "HASHED_PASSWORD_3", + "sshKeys": ["ssh-rsa KEY user3@pc"] + } + ] +