Skip to content

Commit

Permalink
Improve test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
fluffy-critter committed Aug 12, 2020
1 parent 0bf6e06 commit 5cbcb59
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 15 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ preflight:

.PHONY: test
test:
poetry run coverage run -m pytest -v -Werror
poetry run coverage run -m pytest -vv -Werror

.PHONY: cov
cov: test
Expand Down
24 changes: 15 additions & 9 deletions authl/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""

import typing
import uuid
from abc import ABC, abstractmethod

import expiringdict
Expand Down Expand Up @@ -59,29 +60,34 @@ def pop(self, key: str, to_type=tuple) -> typing.Any:

class DictStore(TokenStore):
"""
A token store that stores the token values in a dict-like container.
A token store that stores the values in a dict-like container.
This is suitable for the general case of having a single persistent service
running on a single endpoint.
The default storage is an `expiringdict`_ with a size limit of 1024 and a
maximum lifetime of 1 hour. This can be tuned to your needs. In particular,
the lifetime never needs to be any higher than your longest allowed
transaction lifetime, and the size limit generally needs to be no more than
the number of concurrent logins at any given time.
:param store: dict-like class that maps the generated key to the stored
value. Defaults to an `expiringdict`_ with a size limit of 1024 and a
maximum lifetime of 1 hour. This can be tuned to your needs. In
particular, the lifetime never needs to be any higher than your longest
allowed transaction lifetime, and the size limit generally needs to be
no more than the number of concurrent logins at any given time.
:param func keygen: A function to generate a non-colliding string key for
the stored token. This defaults to py:func:`uuid.uuid4`.
.. _expiringdict: https://pypi.org/project/expiringdict/
"""

def __init__(self, store: dict = None):
def __init__(self, store: dict = None,
keygen: typing.Callable[..., str] = lambda _: str(uuid.uuid4())):
""" Initialize the store """
self._store: dict = expiringdict.ExpiringDict(
max_len=1024,
max_age_seconds=3600) if store is None else store
self._keygen = keygen

def put(self, value):
import uuid
key = str(uuid.uuid4())
key = self._keygen(value)
self._store[key] = value
return key

Expand Down
20 changes: 19 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ coverage = "^5.2"
pytest = "^5.4.3"
requests-mock = "^1.8.0"
sphinx = "^3.1.2"
pytest-mock = "^3.2.0"

[build-system]
requires = ["poetry>=0.12"]
Expand Down
94 changes: 90 additions & 4 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
""" main instance tests """

from authl import Authl
import pytest
import requests_mock

import authl
from authl import Authl, tokens

from . import TestHandler

Expand All @@ -20,6 +24,21 @@ def handles_url(self, url):
return url if url == self.url else None


class LinkHandler(TestHandler):
""" a handler that just handles a page with a particular link rel """

def __init__(self, rel, cid):
self.rel = rel
self.cid = cid

@property
def cb_id(self):
return self.cid

def handles_page(self, url, headers, content, links):
return self.rel in links or content.find('link', rel=self.rel)


def test_register_handler():
""" Test that both registration paths result in the same, correct result """
handler = TestHandler()
Expand All @@ -32,13 +51,80 @@ def test_register_handler():

assert list(instance_1.handlers) == list(instance_2.handlers)

with pytest.raises(ValueError):
instance_2.add_handler(handler)


def test_get_handler_for_url():
""" Test that URL rules map correctly """
handler_1 = UrlHandler('test://foo', 'a')
handler_2 = UrlHandler('test://bar', 'b')
handler_3 = LinkHandler('moo', 'c')
instance = Authl([handler_1, handler_2, handler_3])

with requests_mock.Mocker() as mock:
mock.get('http://moo/link', text='<link rel="moo" href="yes">')
mock.get('http://moo/header', headers={'Link': '<gabba>; rel="moo"'})
mock.get('http://moo/redir', status_code=301, headers={'Location': 'http://moo/header'})
mock.get('http://foo.bar', text="nothing here")

assert instance.get_handler_for_url('test://foo') == (handler_1, 'a', 'test://foo')
assert instance.get_handler_for_url('test://bar') == (handler_2, 'b', 'test://bar')
assert instance.get_handler_for_url('test://baz') == (None, '', '')

assert instance.get_handler_for_url('http://moo/link') == \
(handler_3, 'c', 'http://moo/link')
assert instance.get_handler_for_url('http://moo/header') == \
(handler_3, 'c', 'http://moo/header')
assert instance.get_handler_for_url('http://moo/redir') == \
(handler_3, 'c', 'http://moo/header')

assert instance.get_handler_for_url('http://foo.bar') == (None, '', '')

assert instance.get_handler_for_url('') == (None, '', '')


def test_webmention_url(mocker):
""" test handles_url on a webmention profile """
handler_1 = UrlHandler('test://foo', 'a')
handler_2 = UrlHandler('test://bar', 'b')
instance = Authl([handler_1, handler_2])

assert instance.get_handler_for_url('test://foo') == (handler_1, 'a', 'test://foo')
assert instance.get_handler_for_url('test://bar') == (handler_2, 'b', 'test://bar')
assert instance.get_handler_for_url('test://baz') == (None, '', '')
wgp = mocker.patch('authl.webfinger.get_profiles')
wgp.side_effect = lambda url: {'test://cat', 'test://bar'} if url == '@foo@bar.baz' else {}

assert instance.get_handler_for_url('@foo@bar.baz') == (handler_2, 'b', 'test://bar')


def test_from_config(mocker):
""" Ensure the main from_config function calls the appropriate proxied ones """
test_config = {
'EMAIL_FROM': 'hello',
'FEDIVERSE_NAME': 'hello',
'INDIEAUTH_CLIENT_ID': 'hello',
'TWITTER_CLIENT_KEY': 'hello',
'TEST_ENABLED': True
}

mocks = {}

handler_modules = (('email_addr', tokens.DictStore),
('fediverse', tokens.DictStore),
('indieauth', tokens.DictStore),
('twitter', dict))

for name, _ in handler_modules:
mocks[name] = mocker.patch(f'authl.handlers.{name}.from_config')

mock_test_handler = mocker.patch('authl.handlers.test_handler.TestHandler')

authl.from_config(test_config)

for name, storage_type in handler_modules:
mocks[name].assert_called_once()
config, storage = mocks[name].call_args[0]
assert config == test_config
assert isinstance(storage, storage_type)

mock_test_handler.assert_called_once()
assert mock_test_handler.call_args == (())

0 comments on commit 5cbcb59

Please sign in to comment.