-
Notifications
You must be signed in to change notification settings - Fork 1
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
Tests improvements #40
base: master
Are you sure you want to change the base?
Conversation
…mypy � Conflicts: � lbz/authz.py � lbz/exceptions.py � version
…tests_improvements � Conflicts: � CHANGELOG.md � Makefile � lbz/authentication.py � lbz/authz.py � lbz/dev/misc.py � lbz/dev/server.py � lbz/dev/test.py � lbz/jwt_utils.py � lbz/misc.py � lbz/request.py � lbz/resource.py � lbz/router.py � setup.cfg � tests/fixtures/cognito_auth.py � tests/fixtures/rsa_pair.py � tests/test_authz_authorizer.py � tests/test_authz_decorator.py � tests/test_dev_misc.py � tests/test_dev_server.py � tests/test_resource.py � version
…tests_improvements � Conflicts: � CHANGELOG.md � lbz/authz/authorizer.py � version
Kudos, SonarCloud Quality Gate passed!
|
Released 2021-08-30 | ||
|
||
- Code quality improvements, brings coverage to 99% | ||
- Adds more functional test for DevServer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "... more functional tests ..." - plural form.
|
||
- Code quality improvements, brings coverage to 99% | ||
- Adds more functional test for DevServer | ||
- Improves tests exectuion time (15s -> <5s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "execution"
coverage report --skip-covered -m | ||
|
||
real-coverage: | ||
for file in $$(find lbz -type f \( -name "*.py" -and ! -name "*_.py" -and ! -name "event.py" \)); do \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you take that from the internet or did you prepare that by yourself? 🤔
I am asking because this loop is not that good for the future. At least I see two problems here:
- You skipped here all the files ended by an underscore:
- sometimes there is a logic in
__init__.py
files and then most likely it is tested in the module which is named according to the package - also, there might be a module with a name ending with an underscore, just to avoid the conflict with the installed library e.g.
boto3_.py
orrequests_.py
- sometimes there is a logic in
- You forced the name of the test module and this rule is not written down anywhere (or maybe I haven't seen that yet) - that gives too little freedom in programming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think features like this should be re-thought and introduced separately (not in a pull request which has more than 1k lines added).
But if you want to keep it here, it should at least support splitting tests into the packages:
├── test_<package-1>
│ ├── __init__.py
│ ├── test_<module-1>.py
│ ├── test_<module-2>.py
│ ├── ...
Keep in mind that it will probably extend the time of reviewing/closing this pull request 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its my own, its not a feature its supporting script. We can skip it for now.
There orginal rule was to name tests test_module_its_testing.py.
Its not in the pipeline so we can improve that further in the future.
Spliting the test in packages makes sense but the makefile script is a PITA.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so just remove that or comment out with putting a nice TODO and place the test modules in the proper packages (we have two at this moment: authz
and dev
) 😉 We will return to that with a smaller PR.
@@ -3,7 +3,7 @@ | |||
JWT helpers module. | |||
""" | |||
import json | |||
import os | |||
from os import environ as env |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think renaming the built-in variable is a good idea. That is pretty well-known over the whole world and introducing env
may suggest that here, something different is used for environment variables.
Also, you are not consistent in this change, so I would leave that exactly as it was implemented before 😉
@@ -24,19 +26,21 @@ class MyLambdaDevHandler(BaseHTTPRequestHandler, metaclass=ABCMeta): | |||
@property | |||
@abstractmethod | |||
def cls(self) -> Type[Resource]: | |||
pass | |||
""" | |||
DevServer needs to inherit a Resource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a misleading sentence as the current pattern does not rely on inheriting: resource is just assigned to cls
on the class declaration level.
In the past, I wanted to remove this cls
abstract method at all, or at least write a proper TODO because things like this need to be done simpler (developer-friendly).
@@ -56,9 +60,9 @@ def _get_route_params(self, org_path: str) -> Tuple[Union[str, None], Union[dict | |||
acc += 1 | |||
if len(path) == acc: | |||
return org_route, params | |||
return None, None | |||
raise ValueError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please place here an error message to say directly what happened - it will simplify resolving problems in the future 😉
|
||
def _send_json(self, code: int, obj: dict, headers: dict = None) -> None: | ||
def _send_json(self, code: int, obj: Union[str, dict], headers: dict = None) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I see, obj
is only used at the end of this method, to be JSON dumped. But if it is already a string, that line will produce a different result - I am not sure if it is something that should be accepted, at least it doesn't look so:
>>> json.dumps({"data": []}, indent=4, sort_keys=True)
'{\n "data": []\n}'
>>> json.dumps('{"data": []}', indent=4, sort_keys=True)
'"{\\"data\\": []}"'
@@ -110,15 +110,8 @@ def handle_request(self) -> None: | |||
body=request_obj, | |||
) | |||
) | |||
response = resource() | |||
code = response.status_code | |||
response_as_dict = response.to_dict() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a piece of logic inside the .to_dict()
method which makes sense to me but you skipped it and you always returns raw body
:
body = (
json.dumps(self.body, separators=(",", ":"), default=str)
if self.is_json
else self.body
)
What do you think about that? 🤔
@@ -142,7 +135,7 @@ def do_OPTIONS(self) -> None: # pylint: disable=invalid-name | |||
self.handle_request() | |||
|
|||
|
|||
class MyDevServer: | |||
class MyDevServer(Thread): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you describe in a few words what gives us this inheritance? I can investigate it on my own but I thought it will be simpler/faster to get it from you 😉
|
||
req = Request( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous way how this fixture was built was correct. You shouldn't declare objects like this globally because if it changes, it will affect all the tests that are using it - bugs like that are very hard to find.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request in consider unmutable (except methods but they are not changing the data)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, there are places where this rule is broken e.g. test_resource.py. By using fixtures, we have a much clearer situation because a new instance will be created for each test 😉
|
||
@pytest.fixture(autouse=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
autouse=True
means that this fixture will be used automatically for every test. As not all of them are using sample_request
, I would say, it is too much 😉
@pytest.fixture | ||
def sample_event(): | ||
|
||
@pytest.fixture(autouse=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto: remove autouse=True
return Event( | ||
resource_path="/", | ||
method="GET", | ||
headers={}, | ||
path_params={}, | ||
query_params={}, | ||
body=req, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You put here a Request
but dict
is expected. Sad that mypy
is still disabled for tests, it should catch that automatically.
|
||
|
||
@pytest.fixture(autouse=True) | ||
def user() -> User: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the above fixtures that are built based on global variables may behave differently depending on the usage of the whole code. In my opinion, you should stick to the simple rule: "if you need a global variable, declare a global variable but if you want to have a fixture instead, put the logic in the fixture's body" 😉
Personally, I recommend using fixtures (fewer problems) but definitely, we shouldn't mix those solutions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually in orded to makes test faster we need to fo all jwt related, but we might store them in more "private way"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we agreed offline, the pytest
fixtures with the scope set to session might be the solution in this situation.
} | ||
TOKEN = encode_token(COGNITO_USER) | ||
|
||
with patch("lbz.jwt_utils.PUBLIC_KEYS", [SAMPLE_PUBLIC_KEY]), patch( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Patching things globally makes sense only if the order of creating objects matters. Here, just a User is created, so, it can be easily enclosed in the fixture too 😉
from lbz.router import add_route | ||
|
||
|
||
class SimpleTestResource(Resource): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like those classes are used only for testing the dev
package, am I right?
I will review them based on the test cases but please put it under the test_dev
package, it will be simpler to see the dependencies in this code thanks to that 😉 It can be placed even in the test_dev/conftest.py
module following the pattern.
return encodeded_jwt | ||
|
||
|
||
allowed_audiences = [str(uuid4()), str(uuid4())] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a global variable, it shouldn't be placed in the utils.py
module, and also, it should be named with uppercase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also started thinking about generating those all unique identifiers 🤔 I see that uuid4()
is used often to declare global variables at the top level.
Do we have to do this? Globally? For all the tests? Is it not true that it is needed only for a part of them and the rest can work pretty well with hardcoded values? I would like to introduce some kind of separation here to simplify maintenance in the future 😉
|
||
AUTH_HEADER = Authorizer.sign_authz( | ||
{**_base_auth_payload(), "allow": {"test_res": {"perm-name": {"allow": "*"}}}, "deny": {}}, | ||
SAMPLE_PRIVATE_KEY, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Part of the things is implemented here, part of them in other modules and part under the fixtures
package. We should think about placing them in the right place based on the usage:
lbz/tests
├── test_authz
│ ├── __init__.py
│ ├── conftest.py # all fixtures used for testing authz
│ ├── ...
├── test_dev
│ ├── __init__.py
│ ├── conftest.py # all fixtures used for testing dev
│ ├── ...
├── __init__.py
├── conftest.py # all fixtures used by other tests
├── ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can also think about having constants.py
and utils.py
but only if really needed 😉
# pylint: disable= attribute-defined-outside-init | ||
self.router = Router() | ||
self.router.add_route("/", "GET", "x") | ||
|
||
def teardown_method(self, _test_method): | ||
def teardown_method(self, _test_method: Callable) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could use types.MethodType here instead 😉
Side note: we don't have mypy
enabled for tests, so they are not checked automatically - introducing types into this big pull request may make a review longer 😐
@@ -67,3 +69,7 @@ def test_response_body_passes_through_when_base64(self): | |||
"statusCode": 666, | |||
"isBase64Encoded": True, | |||
} | |||
|
|||
def test_unsported_header_raises(self) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "unsupported"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to place a full sentence too, including the reason - it will be valuable for future developers.
|
||
def test_unsported_header_raises(self) -> None: | ||
with pytest.raises(RuntimeError): | ||
Response(b"xxx", status_code=666) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name of the test is suggesting that the header is tested but here you put body
and the status_code
. So, what exactly is tested here? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the body expects to get str
or dict
but you provided bytes
- mypy
would catch that, I guess.
@@ -37,19 +34,10 @@ def test__init__(self): | |||
|
|||
|
|||
class TestRequest: | |||
def setup_method(self): | |||
@pytest.fixture(autouse=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't mark setup_method
as a fixture - that is a wrong usage of this functionality. You should prepare a normal fixture above which will be used in the tests:
@pytest.fixture
def sample_request(user: User) -> Request:
return Request(
...
user=user,
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, if you want to keep the setup_method
as it is right now, you should prepare a factory that can be used within this method. But it will be rather a normal function - it won't break anything in the pytest
fixtures 😉
nest = NestedDict() | ||
nest["a"]["b"]["c"]["d"]["e"] = "z" | ||
assert nest == {"a": {"b": {"c": {"d": {"e": "z"}}}}} | ||
assert nest["a"]["b"]["c"]["d"]["e"] == "z" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the above is true, this line just tests that Python's dict
is working correctly 😉
I would say, it is not needed but it is just a thought.
assert multi_dict._dict == {} # pylint: disable=protected-access | ||
|
||
|
||
def test_multi_dict_index_error() -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the test implementation, I think the name should be improved, something like:
def test__multi_dict__raises_key_error_if_value_of_key_is_empty_list() -> None
On the other hand, I have to say that this is a bit counterintuitive and I would treat it as a bug but maybe I am wrong - documentation may help here a little 😉
import pytest | ||
|
||
|
||
def not_working_test() -> None: # TODO: finish me! - lbz/jwt_utils.py:26 mocking hell |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use @pytest.mark.skip
to skip tests that you don't want to run. It will be at least shown in the report 😉 Also, it would be good to write a more descriptive TODO, at the first look, I don't have a clue what you wanted to achieve here.
|
||
|
||
@patch("lbz.jwt_utils.PUBLIC_KEYS", [SAMPLE_PUBLIC_KEY]) | ||
@patch("lbz.jwt_utils.ALLOWED_AUDIENCES", allowed_audiences) | ||
class TestAuthentication: | ||
def setup_class(self): | ||
# pylint: disable=attribute-defined-outside-init | ||
@pytest.fixture(autouse=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto: wrong usage of pytest.fixture
sample_user = User(encode_token({"cognito:username": username})) | ||
assert sample_user.__repr__() == f"User username={username}" | ||
sample_user_2 = User(encode_token({"type": "x"})) | ||
assert sample_user_2.__repr__() == "User" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please put these assertions into separate tests - they represent two cases.
with pytest.raises(Unauthorized), patch( | ||
"lbz.jwt_utils.PUBLIC_KEYS", | ||
[{**SAMPLE_PUBLIC_KEY.copy(), "n": str(uuid4())}], | ||
): | ||
User(self.id_token) | ||
|
||
def test_loading_user_parses_user_attributes(self): | ||
parsed = self.cognito_user.copy() | ||
def test_loading_user_parses_user_attributes(self, user_cogniot) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "user_cognito"
class TestResource: | ||
def setup_method(self): | ||
class TestResourceInit: | ||
@pytest.fixture(autouse=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto: wrong usage of pytest
fixtures.
|
||
class TestResource: | ||
def setup_method(self): | ||
class TestResourceInit: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you split the tests of Resource into two classes? 🤔 As for me, having them together makes sense.
# "guest_permissions": {}, | ||
# } | ||
# } | ||
def test___repr__(self) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You put here too many underscores. I know the method contains its own underscores but in general, putting them into the name of the test makes them barely visible (underscores means word separation in the tests) 😉
# } | ||
def test___repr__(self) -> None: | ||
self.res.urn = "/foo/id-12345/bar" | ||
assert str(self.res) == "<Resource GET @ /foo/id-12345/bar >" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you test the __repr__()
method of this class, you should use the built-in repr()
function 😉
str()
depends on both __str__()
and __repr__()
.
class XResource(Resource): | ||
@add_route("/") | ||
def test_method(self): | ||
def test_method(self) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method returns the Response
instance, not None
.
@@ -330,6 +317,15 @@ def test_options_request(self): | |||
ALLOW_ORIGIN_HEADER: ORIGIN_EXAMPLE, | |||
} | |||
|
|||
@patch.object(CORSResource, "resp_headers", return_value={}) | |||
def test_not_allow_oring_in_header(self, resp_headers_mock: MagicMock) -> None: | |||
an_event = defaultdict(MagicMock()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why didn't you use self.make_cors_handler
? It looks like it builds exactly the same thing as you implemented here 🤔
@@ -330,6 +317,15 @@ def test_options_request(self): | |||
ALLOW_ORIGIN_HEADER: ORIGIN_EXAMPLE, | |||
} | |||
|
|||
@patch.object(CORSResource, "resp_headers", return_value={}) | |||
def test_not_allow_oring_in_header(self, resp_headers_mock: MagicMock) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "origin"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, describe it better. I think CORSResource
allows having origin in headers but value can be unacceptable.
an_event["method"] = "GET" | ||
an_event["headers"] = {"origin": "notlocalhost"} | ||
resp = CORSResource(an_event, ["GET", "POST"], origins="localhost")() | ||
assert resp.status_code >= 400 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assertion gives a lot of options, please, choose one status code 😉
@@ -330,6 +317,15 @@ def test_options_request(self): | |||
ALLOW_ORIGIN_HEADER: ORIGIN_EXAMPLE, | |||
} | |||
|
|||
@patch.object(CORSResource, "resp_headers", return_value={}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you mock that? Isn't it better to check the result instead?
@@ -338,15 +334,16 @@ def setup_method(self, _test_method, _init_mock: MagicMock) -> None: | |||
self.resource = PaginatedCORSResource({}, []) | |||
self.resource.path = "/test/path" | |||
self.resource.urn = "/test/path" | |||
req.query_params = MultiDict( | |||
self.req = pytest.mark.usefixtures("sample_request") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't work as you expect. pytest.mark.usefixtures
is a decorator, so you will get here the instance of one of pytest
internal classes. Please. initialize the request normally 😉
|
||
with patch("lbz.authz.authorizer.decode_jwt", lambda _: {**base_auth_payload, **acl}): | ||
assert ( | ||
has_permission(SampleResource(sample_event), "sample_function") is expected_result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function belongs to the authz/decorators.py
module - I think, it would be good to create authz/utils.py
for that if you have a test module named this way 😉
It would also resolve the naming problem of decorators
which I pointed in the previous PR.
) -> None: | ||
sample_event["headers"]["authorization"] = "dont_care" | ||
|
||
with patch("lbz.authz.authorizer.decode_jwt", lambda _: {**base_auth_payload, **acl}): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to return base_auth_payload
if decode_jwt
is mocked? Why not make it simpler:
with patch("lbz.authz.authorizer.decode_jwt", return_value=acl):
sample_event["headers"]["authorization"] = "dont_care" | ||
|
||
with patch("lbz.authz.authorizer.decode_jwt", lambda _: {**base_auth_payload, **acl}): | ||
assert ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the readability: create a variable and then make an assertion 😉
|
||
@patch.dict(environ, env_mock) | ||
class TestAuthorizationDecorator: | ||
@pytest.fixture(autouse=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto: wrong usage of pytest
fixtures.
@patch.dict(environ, env_mock) | ||
class TestAuthorizationDecorator: | ||
@pytest.fixture(autouse=True) | ||
def setup_class(self, auth_header, sample_event) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto: setup_class
should be a class method.
|
||
def test_check_allow_domain(self) -> None: | ||
self.authz.allow = {"test_resource": ALL} | ||
self.authz._check_allow_and_set_resources() # pylint: disable=protected-access |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you test a protected method? That can be used differently inside the class. I think testing check_access()
makes the most sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
becouse here we are testing the authorize module ergo all its private and magick methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I am speaking about the general approach to testing - if something has been marked as protected, you shouldn't use it outside the class/module. If you need to do this, that starts smelling like a problem either in the tests or in the code generally 😬
Of course, you can simply mark that as public (not recommended) or test something that is public and what will be really used by the outside world 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed to disagree offline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As below: we will come back to this topic in the future, because it is an important thing when designing the code 😉
def test__check_resource(self) -> None: | ||
with patch.object(self.authz, "_deny_if_all", new_callable=MagicMock()) as mocked_deny: | ||
self.authz._check_resource({"res": "xxx"}) # pylint: disable=protected-access | ||
mocked_deny.assert_has_calls( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole test is wrongly designed:
_deny_if_all
is a very very simple method, it doesn't have to be mocked._check_resource
is a protected method - shouldn't be tested.- By checking that one protected method triggered another protected method three times, you have frozen the implementation, not the result of the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as above it this module it need to be checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on what you want to achieve, right now, with these tests, there is no chance to change the code without getting errors. In general, that is ok but it blocks the code much - it won't give the possibility to easily refactor the code.
By tests, we should check everything that is important meaning e.g. the result, but it does not matter how the result has been achieved 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed to disagree offline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Temporarily yes, but be aware I will return to this topic sooner or later 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I think that is the last review that I have done for this enormous pull request. My recommendation would be to not touch this branch at all. A much simpler and quicker solution will be to split those changes into several SMALLER pull requests and merge them separately 😉
from lbz.jwt_utils import get_matching_jwk, decode_jwt | ||
from tests import SAMPLE_PUBLIC_KEY, SAMPLE_PRIVATE_KEY | ||
|
||
BAD_PUBLICK_KEY = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "BAD_PUBLIC_KEY"
from tests import SAMPLE_PUBLIC_KEY, SAMPLE_PRIVATE_KEY | ||
|
||
BAD_PUBLICK_KEY = { | ||
"kid": "wrongd75-3f54-4155-84fa-c1a17da22b35", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think simple "wrong-key"
would be better than mixing it with generated-like string 😉
"kid": "wrongd75-3f54-4155-84fa-c1a17da22b35", | ||
} | ||
|
||
ALLOWED_AUDIENCES = [str(uuid4()), str(uuid4())] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw it in utils.py
before, it would be good to declare it once and reuse it then 😉
get_matching_jwk("x") | ||
|
||
|
||
@patch("lbz.jwt_utils.PUBLIC_KEYS", [SAMPLE_PUBLIC_KEY]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those variables bases on the values which are taken directly from the environment variables declared in the __init__.py
module - I don't think it is needed to be set here unless you plan to remove mentioned declaration, do you?
@patch("lbz.jwt_utils.PUBLIC_KEYS", [SAMPLE_PUBLIC_KEY]) | ||
@patch("lbz.jwt_utils.ALLOWED_AUDIENCES", ALLOWED_AUDIENCES) | ||
class TestDecodeJWT: | ||
@patch("lbz.jwt_utils.get_matching_jwk", return_value={}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the implementation and above tests, it looks like it is not possible to get an empty dict from the get_matching_jwk()
function.
assert params is None | ||
|
||
with pytest.raises(ValueError): | ||
handler._get_route_params("") # pylint: disable=protected-access |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the test cases separately.
def test_handle_request(method) -> None: | ||
with patch("socket.socket") as msocket: | ||
msocket.makefile = lambda a, b: io.BytesIO(b"GET / HTTP/1.1\r\n") | ||
msocket.rfile.close = lambda: 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The msocket
variable should become a separate fixture. Mostly, to describe that better but also, to reduce the duplication. The biggest value of the tests is the documentation which can be checked anytime by anyone - so, we need to try to make the tests simple 😉
def test_my_dev_server_run() -> None: | ||
dev_serv = MyDevServer(MyLambdaDevHandlerHelloWorld) | ||
dev_serv.start() | ||
assert dev_serv.is_alive() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't you test that in a separate test module? test_dev_server_running
? 🤔 I think I am getting a little lost. You are mixing the running server with the one which is partially mocked, and with that poor naming of the tests and duplicate part of the code, this won't be maintainable.
I wrote it before, but part of the tests for this library is more like integration tests which should be kept separately and better implemented/documented.
msocket.rfile.close = lambda: 0 | ||
MyLambdaDevHandlerHelloWorld(msocket, ("127.0.0.1", 8888), BaseServer) | ||
|
||
mocked_send.assert_has_calls( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In both test_handle_txt
and test_handle_json
, you are checking if one of the mocked/protected methods was triggered according to the needs. In general, that is not what the test should test 😐 You should rather compare the results which you are getting.
|
||
def hash_string(string): | ||
|
||
def hash_string(string) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function does not return None
.
No description provided.