Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First cut of the a module to derive usernames from 3PIDs #1

Merged
merged 4 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ and phone numbers) when registering users.

From the virtual environment that you use for Synapse, install this module with:
```shell
pip install path/to/synapse-username-from-threepid
pip install synapse-username-from-threepid
```
(If you run into issues, you may need to upgrade `pip` first, e.g. by running
`pip install --upgrade pip`)
Expand All @@ -18,7 +18,15 @@ Then alter your homeserver configuration, adding to your `modules` configuration
modules:
- module: username_from_threepid.UsernameFromThreepid
config:
# TODO: Complete this section with an example for your module
# Which third-party identifier to look for. Can either be "email" (for email
# addresses), or "msisdn" (for phone numbers).
# Required.
threepid_to_use: "email"

# Whether to fail the registration if no third-party identifier was provided during
# the registration process.
# Optional, defaults to false.
fail_if_not_found: true
```


Expand Down Expand Up @@ -77,9 +85,6 @@ Synapse developers (assuming a Unix-like shell):
```

7. If applicable:
Create a *release*, based on the tag you just pushed, on GitHub or GitLab.

8. If applicable:
Create a source distribution and upload it to PyPI:
```shell
python -m build
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dev =
aiounittest
# for type checking
mypy == 0.910
types-mock == 4.0.8
# for linting
black == 21.9b0
flake8 == 4.0.1
Expand Down
42 changes: 14 additions & 28 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,26 @@
from typing import Any, Dict, Optional
from typing import Any, Dict

import attr
from mock import Mock
from synapse.api.errors import SynapseError
from synapse.module_api import ModuleApi

from username_from_threepid import UsernameFromThreepid


@attr.s(auto_attribs=True)
class MockEvent:
"""Mocks an event. Only exposes properties the module uses."""
sender: str
type: str
content: Dict[str, Any]
room_id: str = "!someroom"
state_key: Optional[str] = None

def is_state(self) -> bool:
"""Checks if the event is a state event by checking if it has a state key."""
return self.state_key is not None

@property
def membership(self) -> str:
"""Extracts the membership from the event. Should only be called on an event
that's a membership event, and will raise a KeyError otherwise.
"""
membership: str = self.content["membership"]
return membership

def create_module(
config: Dict[str, Any],
succeed_attempt: int = 1,
) -> UsernameFromThreepid:
module_api = Mock(spec=ModuleApi)

def create_module() -> UsernameFromThreepid:
# 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)
async def check_username(username: str) -> None:
if succeed_attempt != module_api.check_username.call_count:
raise SynapseError(code=400, msg="Username in use", errcode="M_USER_IN_USE")

module_api.check_username = Mock(side_effect=check_username)

# If necessary, give parse_config some configuration to parse.
config = UsernameFromThreepid.parse_config({})
parsed_config = UsernameFromThreepid.parse_config(config)

return UsernameFromThreepid(config, module_api)
return UsernameFromThreepid(parsed_config, module_api)
20 changes: 0 additions & 20 deletions tests/test_example.py

This file was deleted.

122 changes: 122 additions & 0 deletions tests/test_username_from_threepid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright 2022 The Matrix.org Foundation C.I.C.
#
# 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 typing import Optional

import aiounittest

from tests import create_module
from username_from_threepid import LoginType, UsernameFromThreepid


class UsernameFromThreepidTestCase(aiounittest.AsyncTestCase):
async def test_no_3pid(self) -> None:
"""Tests that the module returns None if no 3pid matching the configuration could
be found.
"""
module = create_module({"threepid_to_use": "email"})
res = await module.set_username_from_threepid({}, {})
self.assertIsNone(res)

async def test_no_3pid_exception(self) -> None:
"""Tests that the module raises a RuntimeError if no 3pid matching the
configuration could be found and the configuration requires it.
"""
module = create_module({"threepid_to_use": "email", "fail_if_not_found": True})
with self.assertRaises(RuntimeError):
await module.set_username_from_threepid({}, {})

async def test_email(self) -> None:
"""Tests that email addresses are correctly translated into usernames."""
input = "foo@bar.baz"
expected_output = "foo-bar.baz"
module = create_module({"threepid_to_use": "email"})
await self._test_email(module, input, expected_output)

async def test_email_invalid_char(self) -> None:
"""Tests that characters that would be illegal in an MXID are correctly filtered
out of the resulting username.
"""
input = "fooé@bar.baz"
expected_output = "foo-bar.baz"
module = create_module({"threepid_to_use": "email"})
await self._test_email(module, input, expected_output)

async def test_email_conflict(self) -> None:
"""Tests that, when registering with an email address, a digit is appended if the
resulting username clashes with an existing user.
"""
input = "foo@bar.baz"
expected_output = "foo-bar.baz1"
module = create_module({"threepid_to_use": "email"}, succeed_attempt=2)
await self._test_email(module, input, expected_output)

async def _test_email(
self,
module: UsernameFromThreepid,
input: str,
expected_output: Optional[str],
) -> None:
"""Calls the given module's "set_username_from_threepid" method and checks that
the resulting value matches what's expected.

Args:
module: The module to test.
input: The email address to test with.
expected_output: The expected return value from the module

Raises:
AssertionError if the return value from the module doesn't match the expected
value.
"""
uia_results = {
LoginType.EMAIL_IDENTITY: {
"address": input,
"medium": "email",
"verified_at": 0,
}
}
res = await module.set_username_from_threepid(uia_results, {})
self.assertEqual(res, expected_output)

async def test_msisdn(self) -> None:
"""Tests that registering with a phone number correctly translates into a
username.
"""
msisdn_number = "440000000000"
module = create_module({"threepid_to_use": "msisdn"})
uia_results = {
LoginType.MSISDN: {
"address": msisdn_number,
"medium": "msisdn",
"verified_at": 0,
}
}
res = await module.set_username_from_threepid(uia_results, {})
self.assertEqual(res, msisdn_number)

async def test_msisdn_conflict(self) -> None:
"""Tests that, when registering with an email address, a digit is appended if the
resulting username clashes with an existing user.
"""
msisdn_number = "440000000000"
module = create_module({"threepid_to_use": "msisdn"}, succeed_attempt=2)
uia_results = {
LoginType.MSISDN: {
"address": msisdn_number,
"medium": "msisdn",
"verified_at": 0,
}
}
res = await module.set_username_from_threepid(uia_results, {})
self.assertEqual(res, msisdn_number + "-1")
Loading