diff --git a/conftest.py b/conftest.py index ef8f929..2fde809 100644 --- a/conftest.py +++ b/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch import pytest +from django.db import transaction from fractal.cli.controllers.auth import AuthController from fractal_database.models import Database, Device @@ -64,6 +65,18 @@ def second_test_device(db, test_database): return Device.objects.create(name=unique_id) +@pytest.fixture(scope="function") +def test_target(db, test_database): + from fractal_database_matrix.models import MatrixReplicationTarget + + with transaction.atomic(): + target = MatrixReplicationTarget.objects.create(name="test_target") + device = Device.current_device() + creds = device.matrixcredentials_set.get() + target.matrixcredentials_set.add(creds) + return target + + # @pytest.fixture(scope="function") # def test_matrix_creds(db, test_database): # """ diff --git a/fractal_database_matrix/migrations/0001_initial.py b/fractal_database_matrix/migrations/0001_initial.py index a25bf5b..e82e742 100644 --- a/fractal_database_matrix/migrations/0001_initial.py +++ b/fractal_database_matrix/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.3 on 2024-03-18 20:00 +# Generated by Django 5.0.2 on 2024-03-20 16:06 import django.db.models.deletion import uuid diff --git a/test-config/test_project/test_project/settings.py b/test-config/test_project/test_project/settings.py index 1d283d6..c9e9a79 100644 --- a/test-config/test_project/test_project/settings.py +++ b/test-config/test_project/test_project/settings.py @@ -11,6 +11,7 @@ """ from pathlib import Path + import fractal_database # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -21,7 +22,7 @@ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-+w0b#=7oansriksyd--)c3=ttpi0r2aa)7m)qr5z3+3=a0*zdp' +SECRET_KEY = "django-insecure-+w0b#=7oansriksyd--)c3=ttpi0r2aa)7m)qr5z3+3=a0*zdp" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -32,53 +33,53 @@ # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", *fractal_database.autodiscover_apps(), ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'test_project.urls' +ROOT_URLCONF = "test_project.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'test_project.wsgi.application' +WSGI_APPLICATION = "test_project.wsgi.application" # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -88,16 +89,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -105,9 +106,9 @@ # Internationalization # https://docs.djangoproject.com/en/5.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -117,9 +118,11 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = "static/" # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +FRACTAL_DATABASE_UUID_PK = True diff --git a/tests/test_representations.py b/tests/test_representations.py index 75951ab..d4e12ca 100644 --- a/tests/test_representations.py +++ b/tests/test_representations.py @@ -1,2 +1,293 @@ -async def test_it_works(): - pass +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from asgiref.sync import sync_to_async +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +from django.db import transaction +from fractal.cli.controllers.auth import AuthController, AuthenticatedController +from fractal.matrix.async_client import MatrixClient +from fractal_database.models import ( + Device, + ReplicatedModel, + ReplicationTarget, + RepresentationLog, +) +from fractal_database.representations import Representation +from fractal_database_matrix.models import MatrixCredentials, MatrixReplicationTarget +from fractal_database_matrix.representations import ( + MatrixExistingSubSpace, + MatrixRepresentation, + MatrixRoom, + MatrixSpace, + MatrixSubRoom, + MatrixSubSpace, +) +from nio import RoomGetStateEventResponse, SpaceGetHierarchyResponse + +pytestmark = pytest.mark.django_db(transaction=True) + + +async def test_put_state_no_creds(): + matrix_representation = MatrixRepresentation() + + room_id = "room_id" + target = MagicMock() + state_type = "state_type" + content = {} + + with patch( + "fractal.cli.controllers.auth.AuthenticatedController.get_creds", return_value=None + ): + with pytest.raises(Exception) as e: + await matrix_representation.put_state(room_id, target, state_type, content) + assert str(e.value) == "You must be logged in to put state" + + +async def test_create_room_no_creds(): + matrix_representation = MatrixRepresentation() + + target = MagicMock() + name = "Test Room" + initial_state = [] + public = False + invite = [] + + with patch( + "fractal.cli.controllers.auth.AuthenticatedController.get_creds", return_value=None + ): + with pytest.raises(Exception) as e: + await matrix_representation.create_room( + target, name, initial_state=initial_state, public=public, invite=invite + ) + assert str(e.value) == "You must be logged in to create a room" + + +async def test_create_room_invalid_invite_uppercase( + logged_in_db_auth_controller: AuthenticatedController, +): + matrix_representation = MatrixRepresentation() + creds = AuthenticatedController().get_creds() + + target = MagicMock() + name = "Test Room" + initial_state = [] + public = False + # Simulate uppercase Matrix IDs in the invite list + invite = ["@Test:Upper.com"] + + with patch( + "fractal.cli.controllers.auth.AuthenticatedController.get_creds", return_value=creds + ): + with pytest.raises(Exception) as e: + await matrix_representation.create_room( + target, name, initial_state=initial_state, public=public, invite=invite + ) + assert str(e.value) == "Matrix IDs must be lowercase" + + +# Broken with Update +""" +async def test_create_room_representation_success(): + matrix_representation = MatrixRepresentation() + target = MagicMock() + room_id = "test room" + + with patch( + "fractal.cli.controllers.auth.AuthenticatedController.get_creds", + return_value=("access_token", "homeserver_url", "matrix_id"), + ), patch( + "fractal.matrix.async_client.FractalAsyncClient.room_create", + return_value=MagicMock(room_id="room_id"), + ), patch( + "builtins.print" + ) as mocked_print: + await matrix_representation.create_room(target, room_id, invite=["@test:room.com"]) + + mocked_print.assert_called_with( + f"Successfully created representation of {room_id} in Matrix: room_id" + ) +""" + + +async def test_add_subspace_no_creds(): + matrix_representation = MatrixRepresentation() + + target = MagicMock() + parent_room_id = "parent_room_id" + child_room_id = "child_room_id" + + with patch( + "fractal.cli.controllers.auth.AuthenticatedController.get_creds", return_value=None + ): + with pytest.raises(Exception) as e: + await matrix_representation.add_subspace(target, parent_room_id, child_room_id) + assert str(e.value) == "You must be logged in to add a subspace" + + +# Broken on code update +# not important +""" +async def test_add_subspace_success_print(): + # Mock required objects + matrix_representation = MatrixRepresentation() + target = MagicMock() + parent_room_id = "parent_room_id" + child_room_id = "child_room_id" + + # Patch the necessary method + with patch( + "fractal.cli.controllers.auth.AuthenticatedController.get_creds", + return_value=("access_token", "homeserver_url", "matrix_id"), + ), patch( + "fractal.matrix.async_client.FractalAsyncClient.room_put_state" + ) as mocked_room_put_state, patch( + "builtins.print" + ) as mocked_print: + # Invoke the method + await matrix_representation.add_subspace(target, parent_room_id, child_room_id) + + # Assert that print was called with the correct message + mocked_print.assert_called_with( + f"Successfully added child space {child_room_id} to parent space {parent_room_id}" + ) +""" + + +async def test_create_representation_no_name(): + mock_repr_log = MagicMock() + mock_repr_log.metadata = {} # Empty metadata without "name" + mock_target = MagicMock() + matrix_room = MatrixRoom() + + with patch.object(matrix_room, "create_room") as mock_create_room: + + with pytest.raises(Exception) as e: + await matrix_room.create_representation(mock_repr_log, target_id=mock_target) + + assert str(e.value) == "name must be specified in metadata" + + +async def test_create_representation_success( + logged_in_db_auth_controller: AuthenticatedController, test_database, test_device, test_target +): + target = test_target + + method = "some_method" + + # Creating representation log for testing + repr_log = await RepresentationLog.objects.acreate( + instance=target, + method=method, + target=target, + metadata=target.repr_metadata_props(), + ) + + matrix_room = MatrixRoom() + + result = await matrix_room.create_representation(repr_log=repr_log, target_id=target) + creds = await target.aget_creds() + + async with MatrixClient(target.homeserver, creds.access_token) as client: + res = await client.room_get_state_event(result["room_id"], "m.room.create") + assert isinstance(res, RoomGetStateEventResponse) + + +async def test_create_representation_matrix_space( + logged_in_db_auth_controller: AuthenticatedController, test_database, test_device +): + instance = test_database + target = await MatrixReplicationTarget.objects.acreate(name="test_target") + creds = await test_device.matrixcredentials_set.aget() + await sync_to_async(target.matrixcredentials_set.add)(creds) + method = "some_method" # method not used in tested function + + # Creating representation log for testing + repr_log = await RepresentationLog.objects.acreate( + instance=target, + method=method, + target=target, + metadata=target.repr_metadata_props(), + ) + + matrix_space = MatrixSpace() + + # target.matrixcredentials_set.all.return_value = [] # Mocking empty matrix credentials + + result = await matrix_space.create_representation(repr_log, target) + + assert "room_id" in result + + +def test_create_representation_logs( + logged_in_db_auth_controller: AuthenticatedController, test_database, test_device +): + + instance = test_database.primary_target() + + target = instance + + create_subspace = MatrixSubSpace.create_representation_logs(instance, target) + assert len(create_subspace) == 2 + print(create_subspace[0].method) + assert create_subspace[0].instance == instance + + +async def test_create_subspace_reprsentation( + logged_in_db_auth_controller: AuthenticatedController, test_database, test_device, test_target +): + target = test_target + print("before -----") + # Field id expects a number + primary_target = await test_database.aprimary_target() + target_id = primary_target.pk + method = "some_method" # method not used in tested function + + # Creating representation log for testing + print("target ---", target.metadata["room_id"]) + print("target ---p", primary_target.metadata["room_id"]) + mock_repr_log = await RepresentationLog.objects.acreate( + instance=target, + method=method, + target=primary_target, + metadata=target.repr_metadata_props(), + ) + subspace = MatrixSubSpace() + + result = await subspace.create_representation(repr_log=mock_repr_log, target_id=target_id) + creds = await primary_target.aget_creds() + async with MatrixClient(primary_target.homeserver, creds.access_token) as client: + res = await client.space_get_hierarchy(primary_target.metadata["room_id"]) + print(res) + assert isinstance(res, SpaceGetHierarchyResponse) + room_ids = [room["room_id"] for room in res.rooms] + assert target.metadata["room_id"] in room_ids + + +def test_create_subroom_reprsentation( + logged_in_db_auth_controller: AuthenticatedController, test_database, test_device, test_target +): + target = test_target + primary_target = test_database.primary_target() + target_id = primary_target.pk + method = "some_method" + + subroom = MatrixSubRoom() + + result = subroom.create_representation_logs(instance=target, target=primary_target) + assert len(result) == 2 + assert result[0].instance == target + + +def test_create_existing_subspace_reprsentation( + logged_in_db_auth_controller: AuthenticatedController, test_database, test_device, test_target +): + target = test_target + primary_target = test_database.primary_target() + target_id = primary_target.pk + method = "some_method" + + subspace = MatrixExistingSubSpace() + + result = subspace.create_representation_logs(instance=target, target=primary_target) + # add an assert + assert result[0].instance == target