-
Notifications
You must be signed in to change notification settings - Fork 427
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
Improve add-member API functests #9110
base: main
Are you sure you want to change the base?
Conversation
|
||
assert user in group.members | ||
|
||
def test_it_does_nothing_if_the_user_is_already_a_member_of_the_group( |
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 replaces a previous test that was called test_it_is_idempotent()
. I think this is a better test
|
||
def test_it_returns_404_if_authority_mismatch_on_user( | ||
self, app, factories, auth_client | ||
def test_it_errors_if_the_groups_and_users_authorities_both_dont_match( |
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 dangerous case that was missing from the previous tests: the view code contains a test that the user's and group's authorities match. But what if the user and group's authorities are the same but they both differ from that of the authclient? This could allow authclients to add members to groups outside of the authclient's authority! It turns out there's no bug here because the ADD_MEMBER
permission requires the group's authority to match the authclient's. But still I'm glad to have a functest for this case.
@pytest.mark.xfail | ||
def test_me_alias_with_forwarded_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.
A future PR is going to incidentally add support for the "me" alias to the add-member API at which time this test will pass. For now I added a failing test
def test_it_returns_404_if_missing_auth(self, app, user, db_session, factories): | ||
group = factories.Group() | ||
db_session.commit() | ||
def test_me_alias_with_forwarded_user_with_wrong_authority( |
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.
Just to be sure
@pytest.fixture | ||
def do_request(self, db_session, app, group, user, headers): | ||
def do_request(pubid=group.pubid, userid=user.userid, status=204): | ||
db_session.commit() | ||
return app.post_json( | ||
f"/api/groups/{pubid}/members/{userid}", headers=headers, status=status | ||
) |
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 this "function as fixture" pattern is a good one: reduces duplication and really helps to avoid mistakes in test code. do_request()
will make a successful request to the API unless the test has changed something about the fixtures or passes a non-default value for one of do_request()
's arguments.
It's very easy for error-case tests in this class to forget to commit the DB session and the test can end up passing even though it's not actually testing what it thinks it is. Multiple tests in the class had that problem. Having do_request()
commit the DB session makes this mistake impossible.
third_party_group = factories.Group(authority="thirdparty.com") | ||
headers = self.authorization_header(auth_client) | ||
user2 = factories.User(authority="thirdparty.com") | ||
db_session.commit() |
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 test was completely wrong. It uses third_party_user.userid
in both the URL and the X-Forwarded-User
header and does not include user2
in the request at all. And then asserts that user2
was not added to the group. It's been replaced with a correct test.
): | ||
group = factories.Group() | ||
user = factories.User(authority="somewhere-else.org") |
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.
group
has wrong authority and DB session isn't committed, so test is not testing what it intends to.
): | ||
group = factories.Group() |
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.
group
has wrong authority so test is not testing what it intends to.
): | ||
group = factories.Group(authority="somewhere-else.org") |
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.
user
has wrong authority so test is not testing what it intends to.
|
||
def test_it_returns_404_if_missing_auth(self, app, user, db_session, factories): | ||
group = factories.Group() |
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.
group
and user
have wrong authority so test is not testing what it intends to.
def test_it_returns_404_with_token_auth( | ||
self, app, token_auth_header, user, db_session, factories | ||
): | ||
group = factories.Group() |
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.
group
and user
have wrong authority so test is not testing what it intends to.
55ea33e
to
0a7016c
Compare
The functional tests for the add-member API are a mess: they're in kind of an old-fashioned coding style, but more importantly there are some missing test cases and there are several tests that aren't actually testing what they're supposed to be testing because the test setup is wrong, and at least one of these tests was passing when there was actually a crash in the code. A difficulty is that the h API doesn't return specific error responses. Whenever anything goes wrong with an add-member API request h returns a 404 with the same fixed body that doesn't say specifically *what* was wrong with the request. This means that tests can't assert that they've triggered the error that they intended to trigger. They can only assert that the response is a 404. Several of add-member's functests were actually triggering a *different* 404 than what the test was intending to trigger because the test setup was wrong, but the tests were still passing. For example this test doesn't actually test what it's intending to test: def test_it_returns_404_if_authority_mismatch_on_user( self, app, factories, auth_client ): group = factories.Group() user = factories.User(authority="somewhere-else.org") res = app.post_json( "/api/groups/{pubid}/members/{userid}".format( pubid=group.pubid, userid=user.userid ), headers=self.authorization_header(auth_client), expect_errors=True, ) assert res.status_code == 404 The `group` that the test has created has an authority that doesn't match that of the `auth_client` so the request will 404 for that reason before the code ever gets to the `user`'s authority. In addition the `db_session` hasn't been commited so neither `group` or `user` have been written to the DB, which will also cause the request to 404. The test needs to create a group with the right authority, a user with the wrong authority, and add both to the DB. In this commit I've adopted an approach where class-level test fixtures set up everything that's needed for the API request to succeed. The test for the happy-path doesn't need to do any setup beyond what the fixtures have already done. Tests for error cases then change one particular thing before calling the API. This means the test can be sure that the 404 was triggered by the one thing it changed rather than by anything else. For example: class TestAddMember: def test_it(self, do_request, group, user): do_request() assert user in group.members def test_it_errors_if_the_request_isnt_authenticated(self, do_request, headers): del headers["Authorization"] do_request(status=404) ... @pytest.fixture def authclient(self): """Return a group with an authority matching authclient's.""" @pytest.fixture def headers(self, authclient): """Return an `Authorization` header for authclient.""" @pytest.fixture def group(self, authclient): """Return a group with an authority matching authclient's.""" @pytest.fixture def user(self, authclient): """Return a user with an authority matching authclient's.""" @pytest.fixture def do_request(self, db_session, group, user, headers): def do_request(...): """Commit the db_session then make an API request with group, user and headers.""" return do_request The happy path test (`test_it()`) verifies that, with everything as set up by the test fixtures, the API request succeeds and adds the user to the group. The error test (`test_it_errors_if_the_request_isnt_authenticated()`) changes one thing about the fixtures set up (it deletes the `Authorization` header) and asserts that the request 404s. This approach makes mistakes in tests much less likely. I have also gone through breaking specific parts of the context, security, view and service code and testing that the expected test fails. I did this for every functest in the class. This commit also fixes one bug in the code: if the add-member API is called with an invalid userid (one that isn't in `acct:{username}@{authority}` format) the app crashes. The previous functests actually had a test for this case, but the test was wrong: def test_it_returns_404_if_malformed_userid( self, app, factories, auth_client, db_session ): group = factories.Group() db_session.commit() res = app.post_json( "/api/groups/{pubid}/members/{userid}".format( pubid=group.pubid, userid="[email protected]" ), headers=self.authorization_header(auth_client), expect_errors=True, ) assert res.status_code == 404 This test creates a `group` whose authority doesn't match that of the `auth_client` and the request 404s for that reason before the code ever gets to parsing the malformed userid. So the test passes. In fact if the test had set up the group's authority correctly the app would crash on the malformed userid. This commit replaces this test with a correct one.
0a7016c
to
9ad7e91
Compare
The functional tests for the add-member API are a mess: they're in kind
of an old-fashioned coding style, but more importantly there are some
missing test cases and there are several tests that aren't actually
testing what they're supposed to be testing because the test setup is
wrong, and at least one of these tests was passing when there was
actually a crash in the code.
A difficulty is that the h API doesn't return specific error responses.
Whenever anything goes wrong with an add-member API request h returns a
404 with the same fixed body that doesn't say specifically what was
wrong with the request. This means that tests can't assert that they've
triggered the error that they intended to trigger. They can only assert
that the response is a 404. Several of add-member's functests were
actually triggering a different 404 than what the test was intending
to trigger because the test setup was wrong, but the tests were still
passing.
For example this test doesn't actually test what it's intending to test:
The
group
that the test has created has an authority that doesn'tmatch that of the
auth_client
so the request will 404 for that reasonbefore the code ever gets to the
user
's authority. In addition thedb_session
hasn't been commited so neithergroup
oruser
have beenwritten to the DB, which will also cause the request to 404. The test
needs to create a group with the right authority, a user with the wrong
authority, and add both to the DB.
In this commit I've adopted an approach where class-level test fixtures
set up everything that's needed for the API request to succeed. The
test for the happy-path doesn't need to do any setup beyond what the
fixtures have already done. Tests for error cases then change one
particular thing before calling the API. This means the test can be sure
that the 404 was triggered by the one thing it changed rather than by
anything else. For example:
The happy path test (
test_it()
) verifies that, with everything as setup by the test fixtures, the API request succeeds and adds the user to
the group.
The error test (
test_it_errors_if_the_request_isnt_authenticated()
)changes one thing about the fixtures set up (it deletes the
Authorization
header) and asserts that the request 404s.This approach makes mistakes in tests much less likely.
I have also gone through breaking specific parts of the context,
security, view and service code and testing that the expected test
fails. I did this for every functest in the class.
This commit also fixes one bug in the code: if the add-member API is called
with an invalid userid (one that isn't in
acct:{username}@{authority}
format) the app crashes. The previous functests actually had a test for
this case, but the test was wrong:
This test creates a
group
whose authority doesn't match that of theauth_client
and the request 404s for that reason before the code evergets to parsing the malformed userid. So the test passes. In fact if the
test had set up the group's authority correctly the app would crash on
the malformed userid. This commit replaces this test with a correct one.