Skip to content

Commit f11e7f8

Browse files
committed
tests: make Ninja APIs more testable (bug 2004871) r=zeid
* tests: move Ninja Test Client to conftests * tests: make user fixture more flexible wrt. permissions As there was two `api_client` in use in various tests, the were renamed `ninja_api_client` and `github_api_client` as appropriate. Pull request: #767
1 parent d09e7a8 commit f11e7f8

File tree

3 files changed

+94
-59
lines changed

3 files changed

+94
-59
lines changed

src/lando/conftest.py

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from django.conf import settings
1717
from django.contrib.auth.models import Permission, User
1818
from django.contrib.contenttypes.models import ContentType
19+
from ninja import NinjaAPI
20+
from ninja.testing import TestClient
1921
from requests.models import HTTPError
2022

2123
from lando.api.legacy.stacks import (
@@ -928,16 +930,26 @@ def _create_hg_commit(clone_path: Path) -> str:
928930

929931

930932
@pytest.fixture
931-
def conduit_permissions():
933+
def to_profile_permissions() -> Callable:
934+
"""Convert a list of un-namespaced permissions strings to a list of profile Permissions."""
935+
936+
def _to_profile_permissions(permissions: list[str]) -> list[Permission]:
937+
all_perms = Profile.get_all_scm_permissions()
938+
939+
return [all_perms[p] for p in permissions]
940+
941+
return _to_profile_permissions
942+
943+
944+
@pytest.fixture
945+
def conduit_permissions(to_profile_permissions: Callable) -> list[Permission]:
932946
permissions = (
933947
"scm_level_1",
934948
"scm_level_2",
935949
"scm_level_3",
936950
"scm_conduit",
937951
)
938-
all_perms = Profile.get_all_scm_permissions()
939-
940-
return [all_perms[p] for p in permissions]
952+
return to_profile_permissions(permissions)
941953

942954

943955
@pytest.fixture
@@ -956,23 +968,42 @@ def _instance(scm, **kwargs) -> Worker:
956968

957969

958970
@pytest.fixture
959-
def user_phab_api_key():
960-
return "api-123456789012345678901234567x"
971+
def scm_user() -> Callable:
972+
def scm_user(perms: list[Permission], password: str = "password") -> User:
973+
"""Return a user with the selected Permissions and password."""
974+
user = User.objects.create_user(
975+
username="test_user",
976+
password=password,
977+
email="testuser@example.org",
978+
)
961979

980+
user.profile = Profile(user=user, userinfo={"name": "test user"})
962981

963-
@pytest.fixture
964-
def user(user_plaintext_password, conduit_permissions, user_phab_api_key):
965-
user = User.objects.create_user(
966-
username="test_user",
967-
password=user_plaintext_password,
968-
email="testuser@example.org",
969-
)
982+
for permission in perms:
983+
user.user_permissions.add(permission)
970984

971-
user.profile = Profile(user=user, userinfo={"name": "test user"})
985+
user.save()
986+
user.profile.save()
972987

973-
for permission in conduit_permissions:
974-
user.user_permissions.add(permission)
988+
return user
975989

990+
return scm_user
991+
992+
993+
@pytest.fixture
994+
def user_phab_api_key():
995+
return "api-123456789012345678901234567x"
996+
997+
998+
@pytest.fixture
999+
def user(
1000+
scm_user: Callable,
1001+
user_plaintext_password: str,
1002+
conduit_permissions: list[str],
1003+
user_phab_api_key: str,
1004+
) -> User:
1005+
"""A User with all SCM permissions levels."""
1006+
user = scm_user(conduit_permissions, user_plaintext_password)
9761007
user.profile.save_phabricator_api_key(user_phab_api_key)
9771008

9781009
user.save()
@@ -1340,3 +1371,24 @@ def _mock_response(
13401371
)
13411372

13421373
return _mock_response
1374+
1375+
1376+
@pytest.fixture()
1377+
def ninja_api_client() -> Callable:
1378+
"""Fixture to create a test client for the API."""
1379+
1380+
# XXX If we pass the API directly, we get an error if we want to use this client
1381+
# more than once (regardless of the scope of the fixture), as follows:
1382+
#
1383+
# Looks like you created multiple NinjaAPIs or TestClients
1384+
# To let ninja distinguish them you need to set either unique version or urls_namespace
1385+
# - NinjaAPI(..., version='2.0.0')
1386+
# - NinjaAPI(..., urls_namespace='otherapi')
1387+
#
1388+
# Passing the pre-existing router to the TestClient instead, works. However, getting the
1389+
# router is not golden-path.
1390+
#
1391+
def _ninja_api_client(api: NinjaAPI) -> TestClient:
1392+
return TestClient(api.default_router)
1393+
1394+
return _ninja_api_client

src/lando/try_api/tests/test_authentication.py

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,22 @@
1+
from typing import Callable
12
from unittest.mock import MagicMock, patch
23

34
import pytest
45
from django.contrib.auth.models import User
56
from django.test import Client, override_settings
6-
from ninja.testing import TestClient
77

88
from lando.environments import Environment
99
from lando.try_api.api import api
1010

1111

12-
@pytest.fixture()
13-
def api_client():
14-
"""Fixture to create a test client for the API."""
15-
# XXX If we pass the API directly, we get an error if we want to use this client
16-
# more than once (regardless of the scope of the fixture), as follows:
17-
#
18-
# Looks like you created multiple NinjaAPIs or TestClients
19-
# To let ninja distinguish them you need to set either unique version or urls_namespace
20-
# - NinjaAPI(..., version='2.0.0')
21-
# - NinjaAPI(..., urls_namespace='otherapi')
22-
#
23-
# Passing the pre-existing router to the TestClient instead, works. However, getting the
24-
# router is not golden-path.
25-
#
26-
return TestClient(api._routers[0][1])
27-
28-
2912
@pytest.mark.django_db()
3013
@patch("lando.try_api.api.AccessTokenAuth.authenticate")
3114
def test_authentication_valid_token(
32-
mock_authenticate: MagicMock, api_client: TestClient
15+
mock_authenticate: MagicMock, ninja_api_client: Callable
3316
):
3417
mock_authenticate.return_value = User(username="testuser", is_active=True)
3518

36-
response = api_client.get(
19+
response = ninja_api_client(api).get(
3720
"/__userinfo__",
3821
# The value of the token doesn't actually matter, as the output is controlled by
3922
# the authenticator function, which we mock to return a User.
@@ -45,19 +28,19 @@ def test_authentication_valid_token(
4528

4629

4730
@pytest.mark.django_db()
48-
def test_authentication_no_token(client: Client, api_client: TestClient):
49-
response = api_client.get("/__userinfo__")
31+
def test_authentication_no_token(client: Client, ninja_api_client: Callable):
32+
response = ninja_api_client(api).get("/__userinfo__")
5033
assert response.status_code == 401, "Missing token should result in 401"
5134

5235

5336
@pytest.mark.django_db()
5437
@patch("lando.try_api.api.AccessTokenAuth.authenticate")
5538
def test_authentication_invalid_token(
56-
mock_authenticate: MagicMock, api_client: TestClient
39+
mock_authenticate: MagicMock, ninja_api_client: Callable
5740
):
5841
mock_authenticate.return_value = None
5942

60-
response = api_client.get(
43+
response = ninja_api_client(api).get(
6144
"/__userinfo__",
6245
# The value of the token doesn't actually matter, as the output is controlled by
6346
# the authenticator function, which we mock to return None.
@@ -71,9 +54,9 @@ def test_authentication_invalid_token(
7154
@pytest.mark.django_db()
7255
@override_settings(ENVIRONMENT=Environment("production"))
7356
@patch("lando.try_api.api.AccessTokenAuth.authenticate")
74-
def test_userinfo_not_in_prod(mock_authenticate: MagicMock, api_client: TestClient):
57+
def test_userinfo_not_in_prod(mock_authenticate: MagicMock, ninja_api_client: Callable):
7558
mock_authenticate.return_value = User(username="testuser", is_active=True)
76-
response = api_client.get(
59+
response = ninja_api_client(api).get(
7760
"/__userinfo__", headers={"AuThOrIzAtIoN": "bEaReR valid_token"}
7861
)
7962

src/lando/utils/tests/test_github.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,15 @@ def test_github_authenticated_url_no_token(
158158

159159

160160
def test_github_api_init(mock_github_fetch_token: mock.Mock):
161-
api_client = GitHubAPI("https://github.com/o/r")
161+
github_api_client = GitHubAPI("https://github.com/o/r")
162162

163-
assert api_client.session.headers.get("Authorization") == "Bearer mock_token"
163+
assert github_api_client.session.headers.get("Authorization") == "Bearer mock_token"
164164

165165

166166
def test_github_api_client_init(mock_github_fetch_token: mock.Mock):
167-
api_client = GitHubAPIClient("https://github.com/o/r")
167+
github_api_client = GitHubAPIClient("https://github.com/o/r")
168168

169-
assert api_client.repo_base_url == "repos/o/r"
169+
assert github_api_client.repo_base_url == "repos/o/r"
170170

171171

172172
@pytest.fixture
@@ -378,29 +378,29 @@ def test_api_client_build_pr(
378378
github_pr_diff: str,
379379
github_pr_patch: str,
380380
):
381-
api_client = GitHubAPIClient("https://github.com/mozilla-conduit/test-repo")
381+
github_api_client = GitHubAPIClient("https://github.com/mozilla-conduit/test-repo")
382382

383-
api_client.get_pull_request = mock.MagicMock()
384-
api_client.get_pull_request.return_value = json.loads(github_pr_response)
383+
github_api_client.get_pull_request = mock.MagicMock()
384+
github_api_client.get_pull_request.return_value = json.loads(github_pr_response)
385385

386-
api_client.get_diff = mock.MagicMock()
387-
api_client.get_diff.return_value = github_pr_diff
386+
github_api_client.get_diff = mock.MagicMock()
387+
github_api_client.get_diff.return_value = github_pr_diff
388388

389-
api_client.get_patch = mock.MagicMock()
390-
api_client.get_patch.return_value = github_pr_patch
389+
github_api_client.get_patch = mock.MagicMock()
390+
github_api_client.get_patch.return_value = github_pr_patch
391391

392-
pr = api_client.build_pull_request(1)
392+
pr = github_api_client.build_pull_request(1)
393393

394-
assert api_client.get_pull_request.call_count == 1
394+
assert github_api_client.get_pull_request.call_count == 1
395395
assert pr.number == 1
396396

397397
assert pr.diff == github_pr_diff
398-
assert api_client.get_diff.call_count == 1
399-
assert api_client.get_diff.call_args.args == (1,)
398+
assert github_api_client.get_diff.call_count == 1
399+
assert github_api_client.get_diff.call_args.args == (1,)
400400

401401
assert pr.patch == github_pr_patch
402-
assert api_client.get_patch.call_count == 1
403-
assert api_client.get_patch.call_args.args == (1,)
402+
assert github_api_client.get_patch.call_count == 1
403+
assert github_api_client.get_patch.call_args.args == (1,)
404404

405405

406406
@pytest.fixture

0 commit comments

Comments
 (0)