diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..29c1848 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: Tests + +on: + push: + branches: ["main"] + pull_request: + + +jobs: + check-code-style: + name: Check code style + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - run: python -m pip install tox + - run: tox -e check_codestyle + + # check-types: + # name: Check types with Mypy + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-python@v4 + # with: + # python-version: "3.11" + # - run: python -m pip install tox + # - run: tox -e check_types + + unit-tests: + name: Unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - run: python -m pip install tox + - run: tox -e py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index a9945b0..0000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: Tests - -on: - push: - branches: ["main"] - pull_request: - -jobs: - tests: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.6", "3.x"] - - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - run: pip install tox - - run: tox -e tests \ No newline at end of file diff --git a/.gitignore b/.gitignore index 97ca71f..758d010 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ _trial_temp/ /build/ /dist/ /*.egg-info/ +/.venv/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6c996d0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,83 @@ +[project] +name = "room_access_rules" +description = "Custom room access rules for Tchap." +readme = "README.md" +dynamic = ["version"] + +requires-python = ">=3.8" + +classifiers = [ + "License :: OSI Approved :: Apache Software License" +] + +dependencies = [ + "attrs" +] + +[project.optional-dependencies] +dev = [ + # for tests + "pydantic >= 1.7.4, < 2.0", + "matrix-synapse == 1.98.0", + "tox", + "twisted", + "aiounittest", + # for type checking + "mypy == 1.6.1", + # for linting + "black == 23.10.0", + "ruff == 0.1.1", +] + +[project.urls] +repository = "https://github.com/tchapgouv/synapse-room-access-rules" + +[build-system] +requires = ["setuptools", "setuptools_scm", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] + +[tool.mypy] +strict = true + +[tool.ruff] +line-length = 88 + +# See https://docs.astral.sh/ruff/rules/#error-e +# for error codes. The ones we ignore are: +# E501: Line too long (black enforces this for us) +# E731: do not assign a lambda expression, use a def +# +# flake8-bugbear compatible checks. Its error codes are described at +# https://docs.astral.sh/ruff/rules/#flake8-bugbear-b +# B023: Functions defined inside a loop must not use variables redefined in the loop +ignore = [ + "B023", + "E501", + "E731", +] +select = [ + # pycodestyle + "E", + "W", + # pyflakes + "F", + # flake8-bugbear + "B0", + # flake8-comprehensions + "C4", + # flake8-2020 + "YTT", + # flake8-slots + "SLOT", + # flake8-debugger + "T10", + # flake8-pie + "PIE", + # flake8-executable + "EXE", + # isort + "I", +] + diff --git a/room_access_rules/__init__.py b/room_access_rules/__init__.py index 35fd820..dd58c8b 100644 --- a/room_access_rules/__init__.py +++ b/room_access_rules/__init__.py @@ -17,6 +17,7 @@ from typing import Any, Dict, List, Optional, Tuple import attr +from synapse.api.constants import EventTypes, JoinRules, Membership, RoomCreationPreset from synapse.events import EventBase from synapse.module_api import ModuleApi, UserID from synapse.module_api.errors import ConfigError, SynapseError @@ -27,34 +28,6 @@ ACCESS_RULES_TYPE = "im.vector.room.access_rules" -class EventTypes: - Member = "m.room.member" - Tombstone = "m.room.tombstone" - JoinRules = "m.room.join_rules" - PowerLevels = "m.room.power_levels" - ThirdPartyInvite = "m.room.third_party_invite" - RoomHistoryVisibility = "m.room.history_visibility" - CanonicalAlias = "m.room.canonical_alias" - RoomAvatar = "m.room.avatar" - RoomEncryption = "m.room.encryption" - Topic = "m.room.topic" - Name = "m.room.name" - ServerACL = "m.room.server_acl" - - -class JoinRules: - PUBLIC = "public" - - -class Membership: - INVITE = "invite" - JOIN = "join" - - -class RoomCreationPreset: - PUBLIC_CHAT = "public_chat" - - class AccessRules: DIRECT = "direct" RESTRICTED = "restricted" @@ -364,7 +337,7 @@ async def check_event_allowed( self._is_power_level_content_allowed( event.content, rule, on_room_creation=False ), - None + None, ) if ( diff --git a/scripts-dev/lint.sh b/scripts-dev/lint.sh new file mode 100755 index 0000000..4b44f21 --- /dev/null +++ b/scripts-dev/lint.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Runs linting scripts and type checking +# black - opinionated code formatter +# ruff - lints, finds mistakes, and sorts import statements +# mypy - checks type annotations + +set -e + +files=( + "room_access_rules" + "tests" +) + +# Print out the commands being run +set -x + +black "${files[@]}" +ruff --fix "${files[@]}" +mypy "${files[@]}" diff --git a/setup.py b/setup.py deleted file mode 100644 index 0739ef8..0000000 --- a/setup.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2021 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from setuptools import setup -from codecs import open -import os - -here = os.path.abspath(os.path.dirname(__file__)) - - -def read_file(path_segments): - """Read a UTF-8 file from the package. Takes a list of strings to join to - make the path""" - file_path = os.path.join(here, *path_segments) - with open(file_path, encoding="utf-8") as f: - return f.read() - - -def exec_file(path_segments, name): - """Extract a constant from a python file by looking for a line defining - the constant and executing it.""" - result = {} - code = read_file(path_segments) - lines = [line for line in code.split("\n") if line.startswith(name)] - exec("\n".join(lines), result) - return result[name] - - -setup( - name="synapse-room-access-rules", - packages=["room_access_rules"], - include_package_data=True, - description="Custom room access rules for Tchap", - use_scm_version=True, - setup_requires=["setuptools_scm"], - install_requires=["attrs"], - long_description=read_file(("README.md",)), - long_description_content_type="text/markdown", - url="https://github.com/matrix-org/synapse-room-access-rules", - python_requires=">=3.6", - classifiers=[ - "Development Status :: 4 - Beta", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3", - ], -) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index 9679967..8a5919c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -12,20 +12,20 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import Any, Dict, Optional from unittest.mock import Mock import attr from synapse.module_api import ModuleApi, UserID -from room_access_rules import RoomAccessRules, ACCESS_RULES_TYPE +from room_access_rules import ACCESS_RULES_TYPE, RoomAccessRules PUBLIC_ROOM_ID = "!public:example.com" class MockHttpClient: async def get_json(self, uri, args): - return {"hs": args["address"].split('@')[1]} + return {"hs": args["address"].split("@")[1]} class MockPublicRoomListManager: @@ -41,6 +41,7 @@ def __init__(self, user_id: str): @attr.s(auto_attribs=True) class MockEvent: """Mocks an event. Only exposes properties the module uses.""" + sender: str type: str content: dict @@ -69,15 +70,19 @@ def new_access_rules_event(sender: str, room_id: str, rule: str) -> MockEvent: ) -def create_module(config_override={}) -> RoomAccessRules: +def create_module( + config_override: Optional[Dict[str, Any]] = None, server_name: str = "example.com" +) -> RoomAccessRules: # Create a mock based on the ModuleApi spec, but override some mocked functions # because some capabilities are needed for running the tests. module_api = Mock(spec=ModuleApi) module_api.http_client = MockHttpClient() module_api.public_room_list_manager = MockPublicRoomListManager() + if config_override is None: + config_override = {} config_override["id_server"] = "example.com" config = RoomAccessRules.parse_config(config_override) - return RoomAccessRules(config, module_api) \ No newline at end of file + return RoomAccessRules(config, module_api) diff --git a/tests/test_create_room.py b/tests/test_create_room.py index cedef19..da115da 100644 --- a/tests/test_create_room.py +++ b/tests/test_create_room.py @@ -12,14 +12,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, List, Optional, Any +from typing import Any, Dict, Optional import aiounittest - from synapse.module_api.errors import SynapseError from room_access_rules import ACCESS_RULES_TYPE, AccessRules, EventTypes -from tests import create_module, MockRequester +from tests import MockRequester, create_module class RoomCreateTestCase(aiounittest.AsyncTestCase): @@ -55,14 +54,14 @@ async def test_create_room_invalid_rule(self): """Tests that creating a room with an invalid rule for the creation configuration raises an exception. """ - with self.assertRaises(SynapseError) as cm: + with self.assertRaises(SynapseError): await self._create_room(direct=False, rule=AccessRules.DIRECT) async def test_create_room_direct_invalid_rule(self): """Tests that creating a DM with an invalid rule for the creation configuration raises an exception. """ - with self.assertRaises(SynapseError) as cm: + with self.assertRaises(SynapseError): await self._create_room(direct=True, rule=AccessRules.RESTRICTED) async def test_create_room_default_power_level_rules(self): @@ -102,7 +101,10 @@ async def test_create_room_fails_on_incorrect_power_level_rules(self): with self.assertRaises(SynapseError): await self._create_room( initial_state=[ - {"type": "m.room.power_levels", "content": pl_override_state_default} + { + "type": "m.room.power_levels", + "content": pl_override_state_default, + } ], ) @@ -175,7 +177,7 @@ async def _create_room( "state_key": "", "content": { "rule": rule, - } + }, } ], } @@ -194,7 +196,9 @@ async def _create_room( return config - async def _create_room_and_check_rule(self, expected_rule: str, direct: bool = False): + async def _create_room_and_check_rule( + self, expected_rule: str, direct: bool = False + ): config = await self._create_room(direct=direct, rule=expected_rule) self.assertIsInstance(config["initial_state"], list) diff --git a/tests/test_event_allowed.py b/tests/test_event_allowed.py index 16f376f..461d6a1 100644 --- a/tests/test_event_allowed.py +++ b/tests/test_event_allowed.py @@ -19,10 +19,10 @@ ACCESS_RULES_TYPE, AccessRules, EventTypes, - Membership, JoinRules, + Membership, ) -from tests import create_module, MockEvent, new_access_rules_event +from tests import MockEvent, create_module, new_access_rules_event class SendEventTestCase(aiounittest.AsyncTestCase): @@ -138,12 +138,14 @@ async def test_existing_room_can_change_power_levels(self): ) allowed, _ = await self.module.check_event_allowed( - event=pl_event, state_events=self.direct_room_state, + event=pl_event, + state_events=self.direct_room_state, ) self.assertTrue(allowed) allowed, _ = await self.module.check_event_allowed( - event=pl_event, state_events=self.unrestricted_room_state, + event=pl_event, + state_events=self.unrestricted_room_state, ) self.assertFalse(allowed) @@ -229,7 +231,8 @@ async def test_direct(self): ) allowed, _ = await self.module.check_event_allowed( - event=join_event, state_events=self.direct_room_state, + event=join_event, + state_events=self.direct_room_state, ) self.assertTrue(allowed) @@ -238,11 +241,15 @@ async def test_direct(self): state_with_join[(EventTypes.Member, self.allowed_invitee)] = join_event leave_event = self._new_membership_event( - self.allowed_invitee, self.allowed_invitee, "leave", self.direct_room, + self.allowed_invitee, + self.allowed_invitee, + "leave", + self.direct_room, ) allowed, _ = await self.module.check_event_allowed( - event=leave_event, state_events=state_with_join, + event=leave_event, + state_events=state_with_join, ) self.assertTrue(allowed) @@ -251,11 +258,15 @@ async def test_direct(self): state_with_leave[(EventTypes.Member, self.allowed_invitee)] = leave_event invite_event = self._new_membership_event( - self.room_creator, self.allowed_invitee, Membership.INVITE, self.direct_room, + self.room_creator, + self.allowed_invitee, + Membership.INVITE, + self.direct_room, ) allowed, _ = await self.module.check_event_allowed( - event=invite_event, state_events=state_with_leave, + event=invite_event, + state_events=state_with_leave, ) self.assertTrue(allowed) @@ -293,8 +304,10 @@ async def test_direct(self): # Test that we can send a 3PID invite to a room in which we've always been the # only member. state_with_3pid_invite = state_with_no_invite.copy() - state_with_3pid_invite[(EventTypes.ThirdPartyInvite, "othertoken")] = ( - self._new_3pid_invite(self.room_creator, self.direct_room, token="othertoken") + state_with_3pid_invite[ + (EventTypes.ThirdPartyInvite, "othertoken") + ] = self._new_3pid_invite( + self.room_creator, self.direct_room, token="othertoken" ) # Test that we can't send a 3PID invite to a room in which there's already a 3PID @@ -409,53 +422,65 @@ async def test_change_rules(self): # We can't change the rule from restricted to direct. allowed, _ = await self.module.check_event_allowed( event=new_access_rules_event( - self.room_creator, self.restricted_room, AccessRules.DIRECT, + self.room_creator, + self.restricted_room, + AccessRules.DIRECT, ), - state_events=self.restricted_room_state + state_events=self.restricted_room_state, ) self.assertFalse(allowed) # We can change the rule from restricted to unrestricted. allowed, _ = await self.module.check_event_allowed( event=new_access_rules_event( - self.room_creator, self.restricted_room, AccessRules.UNRESTRICTED, + self.room_creator, + self.restricted_room, + AccessRules.UNRESTRICTED, ), - state_events=self.restricted_room_state + state_events=self.restricted_room_state, ) self.assertTrue(allowed) # We can't change the rule from unrestricted to restricted. allowed, _ = await self.module.check_event_allowed( event=new_access_rules_event( - self.room_creator, self.unrestricted_room, AccessRules.RESTRICTED, + self.room_creator, + self.unrestricted_room, + AccessRules.RESTRICTED, ), - state_events=self.unrestricted_room_state + state_events=self.unrestricted_room_state, ) self.assertFalse(allowed) # We can't change the rule from unrestricted to direct. allowed, _ = await self.module.check_event_allowed( event=new_access_rules_event( - self.room_creator, self.unrestricted_room, AccessRules.DIRECT, + self.room_creator, + self.unrestricted_room, + AccessRules.DIRECT, ), - state_events=self.unrestricted_room_state + state_events=self.unrestricted_room_state, ) self.assertFalse(allowed) # We can't change the rule from direct to restricted. allowed, _ = await self.module.check_event_allowed( event=new_access_rules_event( - self.room_creator, self.direct_room, AccessRules.RESTRICTED, + self.room_creator, + self.direct_room, + AccessRules.RESTRICTED, ), - state_events=self.direct_room_state + state_events=self.direct_room_state, ) self.assertFalse(allowed) allowed, _ = await self.module.check_event_allowed( event=new_access_rules_event( - self.room_creator, self.direct_room, AccessRules.UNRESTRICTED, + self.room_creator, + self.direct_room, + AccessRules.UNRESTRICTED, ), - state_events=self.direct_room_state + state_events=self.direct_room_state, ) self.assertFalse(allowed) @@ -469,7 +494,12 @@ async def test_change_room_avatar(self): type=EventTypes.RoomAvatar, state_key="", content={ - "info": {"h": 398, "mimetype": "image/jpeg", "size": 31037, "w": 394}, + "info": { + "h": 398, + "mimetype": "image/jpeg", + "size": 31037, + "w": 394, + }, "url": "mxc://example.org/JWEIFJgwEIhweiWJE", }, ), @@ -528,7 +558,9 @@ async def test_revoke_3pid_invite_direct(self): # Check that the invite is allowed. invite_event = self._new_3pid_invite( - self.room_creator, self.direct_room, invite_content, + self.room_creator, + self.direct_room, + invite_content, ) allowed, _ = await self.module.check_event_allowed( @@ -539,12 +571,16 @@ async def test_revoke_3pid_invite_direct(self): self.assertTrue(allowed) # Add the invite into the room's state so we can revoke it. - state_events[(EventTypes.ThirdPartyInvite, invite_event.state_key)] = invite_event + state_events[ + (EventTypes.ThirdPartyInvite, invite_event.state_key) + ] = invite_event # Check that the module understands a revocation of the invite as such, and not as # a new invite. invite_event = self._new_3pid_invite( - self.room_creator, self.direct_room, {}, + self.room_creator, + self.direct_room, + {}, ) allowed, _ = await self.module.check_event_allowed( @@ -554,13 +590,18 @@ async def test_revoke_3pid_invite_direct(self): self.assertTrue(allowed) - state_events[(EventTypes.ThirdPartyInvite, invite_event.state_key)] = invite_event + state_events[ + (EventTypes.ThirdPartyInvite, invite_event.state_key) + ] = invite_event # Check that the revoked invite is ignored when processing a new invite - if it # isn't then the module would reject it since it would think we're trying to send # a second invite in a DM, which is forbidden. invite_event = self._new_3pid_invite( - self.room_creator, self.direct_room, invite_content, "someothertoken", + self.room_creator, + self.direct_room, + invite_content, + "someothertoken", ) allowed, _ = await self.module.check_event_allowed( @@ -590,12 +631,12 @@ async def test_forbidden_users_join(self): ) state_events = self.restricted_room_state.copy() - state_events[(EventTypes.Member, self.forbidden_invitee)] = ( - self._new_membership_event( - self.room_creator, - self.forbidden_invitee, - Membership.INVITE, - ) + state_events[ + (EventTypes.Member, self.forbidden_invitee) + ] = self._new_membership_event( + self.room_creator, + self.forbidden_invitee, + Membership.INVITE, ) # Check that a forbidden user cannot join a restricted room, even with an invite. @@ -604,30 +645,33 @@ async def test_forbidden_users_join(self): # Check that an allowed user can join a restricted room, even without an invite. allowed, _ = await self.module.check_event_allowed( - event=allowed_join, state_events=self.restricted_room_state, + event=allowed_join, + state_events=self.restricted_room_state, ) self.assertTrue(allowed) # Check that a forbidden user cannot join an unrestricted room if they haven't # been invited into it. allowed, _ = await self.module.check_event_allowed( - event=forbidden_join, state_events=self.unrestricted_room_state, + event=forbidden_join, + state_events=self.unrestricted_room_state, ) self.assertFalse(allowed) state_events = self.unrestricted_room_state.copy() - state_events[(EventTypes.Member, self.forbidden_invitee)] = ( - self._new_membership_event( - self.room_creator, - self.forbidden_invitee, - Membership.INVITE, - ) + state_events[ + (EventTypes.Member, self.forbidden_invitee) + ] = self._new_membership_event( + self.room_creator, + self.forbidden_invitee, + Membership.INVITE, ) # Check that a forbidden user can join an unrestricted room if they have been # invited into it. allowed, _ = await self.module.check_event_allowed( - event=forbidden_join, state_events=state_events, + event=forbidden_join, + state_events=state_events, ) self.assertTrue(allowed) @@ -661,7 +705,7 @@ def _new_3pid_invite( sender: str, room_id: str, content: Optional[dict] = None, - token: str = "sometoken" + token: str = "sometoken", ) -> MockEvent: return MockEvent( sender=sender, @@ -689,4 +733,3 @@ async def _test_allowed_except_direct(self, event: MockEvent): state_events=self.direct_room_state, ) self.assertFalse(allowed) - diff --git a/tests/test_visibility.py b/tests/test_visibility.py index af6dee9..65912d0 100644 --- a/tests/test_visibility.py +++ b/tests/test_visibility.py @@ -16,7 +16,7 @@ import aiounittest from room_access_rules import ACCESS_RULES_TYPE, AccessRules -from tests import create_module, PUBLIC_ROOM_ID, new_access_rules_event +from tests import PUBLIC_ROOM_ID, create_module, new_access_rules_event class RoomVisibilityTestCase(aiounittest.AsyncTestCase): @@ -30,14 +30,18 @@ async def test_change_rules_when_published(self): """ state = { (ACCESS_RULES_TYPE, ""): new_access_rules_event( - self.user_id, PUBLIC_ROOM_ID, AccessRules.RESTRICTED, + self.user_id, + PUBLIC_ROOM_ID, + AccessRules.RESTRICTED, ) } # Check that we can't change the rule to 'unrestricted'. allowed, _ = await self.module.check_event_allowed( event=new_access_rules_event( - self.user_id, PUBLIC_ROOM_ID, AccessRules.UNRESTRICTED, + self.user_id, + PUBLIC_ROOM_ID, + AccessRules.UNRESTRICTED, ), state_events=state, ) @@ -47,7 +51,9 @@ async def test_change_rules_when_published(self): # Check that we can't change the rule to 'direct'. allowed, _ = await self.module.check_event_allowed( event=new_access_rules_event( - self.user_id, PUBLIC_ROOM_ID, AccessRules.DIRECT, + self.user_id, + PUBLIC_ROOM_ID, + AccessRules.DIRECT, ), state_events=state, ) @@ -57,31 +63,37 @@ async def test_change_rules_when_published(self): async def test_change_visibility_direct(self): """Tests that direct rooms can't be published to the room directory.""" await self._test_change_visibility_to_public( - rule=AccessRules.DIRECT, expect_allowed=False, + rule=AccessRules.DIRECT, + expect_allowed=False, ) async def test_change_visibility_restricted(self): """Tests that restricted rooms can be published to the room directory.""" await self._test_change_visibility_to_public( - rule=AccessRules.RESTRICTED, expect_allowed=True, + rule=AccessRules.RESTRICTED, + expect_allowed=True, ) async def test_change_visibility_unrestricted(self): """Tests that unrestricted rooms can't be published to the room directory.""" await self._test_change_visibility_to_public( - rule=AccessRules.UNRESTRICTED, expect_allowed=False, + rule=AccessRules.UNRESTRICTED, + expect_allowed=False, ) async def test_change_visibility_no_rule(self): """Tests that rooms without a rule can be published to the room directory, since - their rooms are assumed to be 'restricted'. - """ + their rooms are assumed to be 'restricted'. + """ await self._test_change_visibility_to_public( - rule=None, expect_allowed=True, + rule=None, + expect_allowed=True, ) async def _test_change_visibility_to_public( - self, rule: Optional[str], expect_allowed: bool, + self, + rule: Optional[str], + expect_allowed: bool, ): """Test if a room with the given rule can be published to the server's room directory. @@ -96,10 +108,14 @@ async def _test_change_visibility_to_public( state = {} if rule is not None: state[(ACCESS_RULES_TYPE, "")] = new_access_rules_event( - self.user_id, room_id, rule, + self.user_id, + room_id, + rule, ) self.assertEqual( - await self.module.check_visibility_can_be_modified(room_id, state, "public"), + await self.module.check_visibility_can_be_modified( + room_id, state, "public" + ), expect_allowed, ) diff --git a/tox.ini b/tox.ini index 7ff0ec0..c59f268 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,27 @@ [tox] -envlist = tests +envlist = py, check_codestyle, check_types -[testenv:tests] -deps = - matrix-synapse>=1.39.0 - aiounittest>=1.4.0 +# required for PEP 517 (pyproject.toml-style) builds +isolated_build = true + +[testenv:py] + +extras = dev + +commands = + python -m twisted.trial tests + +[testenv:check_codestyle] + +extras = dev + +commands = + - black --check --diff room_access_rules tests + - ruff --diff room_access_rules tests + +[testenv:check_types] + +extras = dev commands = - python -m unittest discover \ No newline at end of file + mypy room_access_rules tests