Tests should not depend on Matrix-nio, that uses lxml that needs a lot of dependencies and a lot of time to build

This commit is contained in:
Yohann D'ANELLO 2020-11-03 20:52:55 +01:00
parent 1ddf39f296
commit 04dd02b88a
11 changed files with 110 additions and 85 deletions

View File

@ -6,7 +6,7 @@ py38:
stage: test stage: test
image: python:3.8-alpine image: python:3.8-alpine
before_script: before_script:
- apk add --no-cache gcc libc-dev libffi-dev libmagic libxml2-dev libxslt-dev libxml2-dev libxslt-dev - apk add --no-cache libmagic
- pip install tox --no-cache-dir - pip install tox --no-cache-dir
script: tox -e py38 script: tox -e py38
@ -14,7 +14,7 @@ py39:
stage: test stage: test
image: python:3.9-alpine image: python:3.9-alpine
before_script: before_script:
- apk add --no-cache gcc libc-dev libffi-dev libmagic libxml2-dev libxslt-dev libxml2-dev libxslt-dev - apk add --no-cache gcc libmagic
- pip install tox --no-cache-dir - pip install tox --no-cache-dir
script: tox -e py39 script: tox -e py39

View File

@ -11,7 +11,7 @@ RUN apk add --no-cache bash
RUN mkdir /code RUN mkdir /code
WORKDIR /code WORKDIR /code
COPY requirements.txt /code/requirements.txt COPY requirements.txt /code/requirements.txt
RUN pip install -r requirements.txt psycopg2-binary sympasoap --no-cache-dir RUN pip install -r requirements.txt --no-cache-dir
COPY . /code/ COPY . /code/

View File

@ -1,9 +1,8 @@
import os import os
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from corres2math.matrix import Matrix, RoomVisibility, UploadError from corres2math.matrix import Matrix, RoomPreset, RoomVisibility
from django.core.management import BaseCommand from django.core.management import BaseCommand
from nio import RoomPreset
from registration.models import AdminRegistration, Registration from registration.models import AdminRegistration, Registration
@ -11,19 +10,23 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
Matrix.set_display_name("Bot des Correspondances") Matrix.set_display_name("Bot des Correspondances")
if not os.path.isfile(".matrix_avatar"): # pragma: no cover if not os.getenv("SYNAPSE_PASSWORD"):
stat_file = os.stat("corres2math/static/logo.png") avatar_uri = "plop"
with open("corres2math/static/logo.png", "rb") as f: else: # pragma: no cover
resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png", filesize=stat_file.st_size) if not os.path.isfile(".matrix_avatar"):
if isinstance(resp, UploadError): stat_file = os.stat("corres2math/static/logo.png")
raise Exception(resp) with open("corres2math/static/logo.png", "rb") as f:
avatar_uri = resp.content_uri resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png",
with open(".matrix_avatar", "w") as f: filesize=stat_file.st_size)
f.write(avatar_uri) if not hasattr(resp, "content_uri"):
Matrix.set_avatar(avatar_uri) raise Exception(resp)
avatar_uri = resp.content_uri
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
Matrix.set_avatar(avatar_uri)
with open(".matrix_avatar", "r") as f: with open(".matrix_avatar", "r") as f:
avatar_uri = f.read().rstrip(" \t\r\n") avatar_uri = f.read().rstrip(" \t\r\n")
if not async_to_sync(Matrix.resolve_room_alias)("#faq:correspondances-maths.fr"): if not async_to_sync(Matrix.resolve_room_alias)("#faq:correspondances-maths.fr"):
Matrix.create_room( Matrix.create_room(

View File

@ -2,7 +2,7 @@ import os
import re import re
from corres2math.lists import get_sympa_client from corres2math.lists import get_sympa_client
from corres2math.matrix import Matrix from corres2math.matrix import Matrix, RoomPreset, RoomVisibility
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models from django.db import models
@ -13,7 +13,6 @@ from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.text import format_lazy from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from nio import RoomPreset, RoomVisibility
class Team(models.Model): class Team(models.Model):

View File

@ -1,4 +1,3 @@
import os
from datetime import timedelta from datetime import timedelta
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -705,7 +704,6 @@ class TestStudentParticipation(TestCase):
call_command('fix_matrix_channels') call_command('fix_matrix_channels')
call_command('setup_third_phase') call_command('setup_third_phase')
os.remove(".matrix_avatar")
class TestAdmin(TestCase): class TestAdmin(TestCase):

View File

@ -12,7 +12,7 @@ from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode from django.utils.http import urlsafe_base64_encode
from .auth import CustomAuthUser from .auth import CustomAuthUser
from .models import AdminRegistration, CoachRegistration, Registration, StudentRegistration from .models import AdminRegistration, CoachRegistration, StudentRegistration
class TestIndexPage(TestCase): class TestIndexPage(TestCase):

View File

@ -1,12 +1,7 @@
from enum import Enum
import os import os
from typing import Any, Dict, Optional, Tuple, Union
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from nio import AsyncClient, DataProvider, ProfileSetAvatarError, ProfileSetAvatarResponse, \
ProfileSetDisplayNameError, ProfileSetDisplayNameResponse, RoomCreateError, RoomCreateResponse, \
RoomInviteError, RoomInviteResponse, RoomKickError, RoomKickResponse, RoomPreset, \
RoomPutStateError, RoomPutStateResponse, RoomResolveAliasResponse, RoomVisibility, TransferMonitor, \
UploadError, UploadResponse
class Matrix: class Matrix:
@ -18,11 +13,11 @@ class Matrix:
Tasks are normally asynchronous, but for compatibility we make Tasks are normally asynchronous, but for compatibility we make
them synchronous. them synchronous.
""" """
_token: str = None _token = None
_device_id: str = None _device_id = None
@classmethod @classmethod
async def _get_client(cls) -> Union[AsyncClient, "FakeMatrixClient"]: # pragma: no cover async def _get_client(cls): # pragma: no cover
""" """
Retrieve the bot account. Retrieve the bot account.
If not logged, log in and store access token. If not logged, log in and store access token.
@ -30,6 +25,7 @@ class Matrix:
if not os.getenv("SYNAPSE_PASSWORD"): if not os.getenv("SYNAPSE_PASSWORD"):
return FakeMatrixClient() return FakeMatrixClient()
from nio import AsyncClient
client = AsyncClient("https://correspondances-maths.fr", "@corres2mathbot:correspondances-maths.fr") client = AsyncClient("https://correspondances-maths.fr", "@corres2mathbot:correspondances-maths.fr")
client.user_id = "@corres2mathbot:correspondances-maths.fr" client.user_id = "@corres2mathbot:correspondances-maths.fr"
@ -53,7 +49,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @async_to_sync
async def set_display_name(cls, name: str) -> Union[ProfileSetDisplayNameResponse, ProfileSetDisplayNameError]: async def set_display_name(cls, name: str):
""" """
Set the display name of the bot account. Set the display name of the bot account.
""" """
@ -62,7 +58,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @async_to_sync
async def set_avatar(cls, avatar_url: str) -> Union[ProfileSetAvatarResponse, ProfileSetAvatarError]: async def set_avatar(cls, avatar_url: str): # pragma: no cover
""" """
Set the display avatar of the bot account. Set the display avatar of the bot account.
""" """
@ -73,13 +69,13 @@ class Matrix:
@async_to_sync @async_to_sync
async def upload( async def upload(
cls, cls,
data_provider: DataProvider, data_provider,
content_type: str = "application/octet-stream", content_type: str = "application/octet-stream",
filename: Optional[str] = None, filename: str = None,
encrypt: bool = False, encrypt: bool = False,
monitor: Optional[TransferMonitor] = None, monitor=None,
filesize: Optional[int] = None, filesize: int = None,
) -> Tuple[Union[UploadResponse, UploadError], Optional[Dict[str, Any]]]: ): # pragma: no cover
""" """
Upload a file to the content repository. Upload a file to the content repository.
@ -134,24 +130,24 @@ class Matrix:
""" """
client = await cls._get_client() client = await cls._get_client()
return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) \ return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) \
if isinstance(client, AsyncClient) else UploadResponse("debug mode"), None if not isinstance(client, FakeMatrixClient) else None, None
@classmethod @classmethod
@async_to_sync @async_to_sync
async def create_room( async def create_room(
cls, cls,
visibility: RoomVisibility = RoomVisibility.private, visibility=None,
alias: Optional[str] = None, alias=None,
name: Optional[str] = None, name=None,
topic: Optional[str] = None, topic=None,
room_version: Optional[str] = None, room_version=None,
federate: bool = True, federate=True,
is_direct: bool = False, is_direct=False,
preset: Optional[RoomPreset] = None, preset=None,
invite=(), invite=(),
initial_state=(), initial_state=(),
power_level_override: Optional[Dict[str, Any]] = None, power_level_override=None,
) -> Union[RoomCreateResponse, RoomCreateError]: ):
""" """
Create a new room. Create a new room.
@ -213,18 +209,18 @@ class Matrix:
power_level_override) power_level_override)
@classmethod @classmethod
async def resolve_room_alias(cls, room_alias: str) -> Optional[str]: async def resolve_room_alias(cls, room_alias: str):
""" """
Resolve a room alias to a room ID. Resolve a room alias to a room ID.
Return None if the alias does not exist. Return None if the alias does not exist.
""" """
client = await cls._get_client() client = await cls._get_client()
resp: RoomResolveAliasResponse = await client.room_resolve_alias(room_alias) resp = await client.room_resolve_alias(room_alias)
return resp.room_id if isinstance(resp, RoomResolveAliasResponse) else None return resp.room_id if resp else None
@classmethod @classmethod
@async_to_sync @async_to_sync
async def invite(cls, room_id: str, user_id: str) -> Union[RoomInviteResponse, RoomInviteError]: async def invite(cls, room_id: str, user_id: str):
""" """
Invite a user to a room. Invite a user to a room.
@ -266,13 +262,13 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @async_to_sync
async def kick(cls, room_id: str, user_id: str, reason: str = None) -> Union[RoomKickResponse, RoomKickError]: async def kick(cls, room_id: str, user_id: str, reason: str = None):
""" """
Kick a user from a room, or withdraw their invitation. Kick a user from a room, or withdraw their invitation.
Kicking a user adjusts their membership to "leave" with an optional Kicking a user adjusts their membership to "leave" with an optional
reason. reason.
²
Returns either a `RoomKickResponse` if the request was successful or Returns either a `RoomKickResponse` if the request was successful or
a `RoomKickError` if there was an error with the request. a `RoomKickError` if there was an error with the request.
@ -289,8 +285,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @async_to_sync
async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int)\ async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int): # pragma: no cover
-> Union[RoomPutStateResponse, RoomPutStateError]: # pragma: no cover
""" """
Put a given power level to a user in a certain room. Put a given power level to a user in a certain room.
@ -306,7 +301,7 @@ class Matrix:
""" """
client = await cls._get_client() client = await cls._get_client()
if isinstance(client, FakeMatrixClient): if isinstance(client, FakeMatrixClient):
return RoomPutStateError("debug mode") return None
if room_id.startswith("#"): if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id) room_id = await cls.resolve_room_alias(room_id)
@ -317,8 +312,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @async_to_sync
async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int)\ async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int): # pragma: no cover
-> Union[RoomPutStateResponse, RoomPutStateError]: # pragma: no cover
""" """
Define the minimal power level to have to send a certain event type Define the minimal power level to have to send a certain event type
in a given room. in a given room.
@ -335,7 +329,7 @@ class Matrix:
""" """
client = await cls._get_client() client = await cls._get_client()
if isinstance(client, FakeMatrixClient): if isinstance(client, FakeMatrixClient):
return RoomPutStateError("debug mode") return None
if room_id.startswith("#"): if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id) room_id = await cls.resolve_room_alias(room_id)
@ -349,8 +343,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @async_to_sync
async def set_room_avatar(cls, room_id: str, avatar_uri: str)\ async def set_room_avatar(cls, room_id: str, avatar_uri: str):
-> Union[RoomPutStateResponse, RoomPutStateError]:
""" """
Define the avatar of a room. Define the avatar of a room.
@ -370,6 +363,22 @@ class Matrix:
}, state_key="") }, state_key="")
if os.getenv("SYNAPSE_PASSWORD"): # pragma: no cover
from nio import RoomVisibility, RoomPreset
RoomVisibility = RoomVisibility
RoomPreset = RoomPreset
else:
# When running tests, faking matrix-nio classes to don't include the module
class RoomVisibility(Enum):
private = 'private'
public = 'public'
class RoomPreset(Enum):
private_chat = "private_chat"
trusted_private_chat = "trusted_private_chat"
public_chat = "public_chat"
class FakeMatrixClient: class FakeMatrixClient:
""" """
Simulate a Matrix client to run tests, if no Matrix homeserver is connected. Simulate a Matrix client to run tests, if no Matrix homeserver is connected.

View File

@ -2,7 +2,6 @@ from threading import local
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.models import AnonymousUser, User
from django.contrib.sessions.backends.db import SessionStore
USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session')

View File

@ -51,16 +51,14 @@ INSTALLED_APPS = [
'django.forms', 'django.forms',
'bootstrap_datepicker_plus', 'bootstrap_datepicker_plus',
'cas_server',
'crispy_forms', 'crispy_forms',
'django_extensions',
'django_tables2', 'django_tables2',
'haystack', 'haystack',
'logs', 'logs',
'mailer',
'polymorphic', 'polymorphic',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'cas_server',
'api', 'api',
'eastereggs', 'eastereggs',
@ -68,6 +66,12 @@ INSTALLED_APPS = [
'participation', 'participation',
] ]
if "test" not in sys.argv: # pragma: no cover
INSTALLED_APPS += [
'django_extensions',
'mailer',
]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',

View File

@ -1,17 +1,19 @@
Django~=3.0 Django~=3.1
django-bootstrap-datepicker-plus django-bootstrap-datepicker-plus~=3.0
django-cas-server django-cas-server~=1.2
django-crispy-forms django-crispy-forms~=1.9
django-extensions django-extensions~=3.0
django-filter~=2.3.0 django-filter~=2.3
django-haystack~=3.0 django-haystack~=3.0
django-mailer django-mailer~=2.0
django-polymorphic django-polymorphic~=3.0
django-tables2 django-tables2~=2.3
djangorestframework~=3.11.1 djangorestframework~=3.12
django-rest-polymorphic django-rest-polymorphic~=0.1
matrix-nio gunicorn~=20.0
ptpython matrix-nio~=0.15
python-magic~=0.4.18 psycopg2-binary~=2.8
gunicorn ptpython~=3.0
whoosh python-magic~=0.4
sympasoap~=1.0
whoosh~=2.7

15
tox.ini
View File

@ -7,10 +7,21 @@ envlist =
skipsdist = True skipsdist = True
[testenv] [testenv]
sitepackages = True sitepackages = False
deps = deps =
-r{toxinidir}/requirements.txt
coverage coverage
Django~=3.1
django-bootstrap-datepicker-plus~=3.0
django-cas-server~=1.2
django-crispy-forms~=1.9
django-filter~=2.3
django-haystack~=3.0
django-polymorphic~=3.0
django-tables2~=2.3
djangorestframework~=3.12
django-rest-polymorphic~=0.1
python-magic~=0.4
whoosh~=2.7
commands = commands =
coverage run --source=apps,corres2math ./manage.py test apps/ corres2math/ coverage run --source=apps,corres2math ./manage.py test apps/ corres2math/
coverage report -m coverage report -m