• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ansible / awx-plugins / 28891854405

07 Jul 2026 07:11PM UTC coverage: 76.693%. Remained the same
28891854405

Pull #192

github

web-flow
Merge e748cc93b into 7c6a7cf97
Pull Request #192: Remove redundant linters, relax over-aggressive checks

39 of 39 branches covered (100.0%)

Branch coverage included in aggregate %.

65 of 68 new or added lines in 18 files covered. (95.59%)

2929 of 3831 relevant lines covered (76.46%)

1.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.63
/tests/github_app_test.py
1
"""Tests for GitHub App Installation access token extraction plugin."""
2

3
from typing import TypedDict
2✔
4

5
import pytest
2✔
6
from pytest_mock import MockerFixture
2✔
7

8
from cryptography.hazmat.backends import default_backend
2✔
9
from cryptography.hazmat.primitives.asymmetric.rsa import (
2✔
10
    RSAPrivateKey,
11
    RSAPublicKey,
12
    generate_private_key,
13
)
14
from cryptography.hazmat.primitives.serialization import (
2✔
15
    Encoding,
16
    NoEncryption,
17
    PrivateFormat,
18
    PublicFormat,
19
)
20
from github.Auth import AppInstallationAuth
2✔
21
from github.Consts import DEFAULT_JWT_ALGORITHM
2✔
22
from github.GithubException import (
2✔
23
    BadAttributeException,
24
    GithubException,
25
    UnknownObjectException,
26
)
27
from jwt import decode as decode_jwt
2✔
28

29
from awx_plugins.credentials import github_app as gh_app_plugin_mod
2✔
30

31

32
RSA_PUBLIC_EXPONENT = 65_537
2✔
33
MINIMUM_RSA_KEY_SIZE = 1024  # the lowest value chosen for performance in tests
2✔
34
TEST_APP_ID = 123
2✔
35

36

37
@pytest.fixture(scope='module')
2✔
38
def rsa_private_key() -> RSAPrivateKey:
2✔
39
    """Generate an RSA private key."""
40
    return generate_private_key(
×
41
        public_exponent=RSA_PUBLIC_EXPONENT,
2✔
42
        key_size=MINIMUM_RSA_KEY_SIZE,  # would be 4096 or higher in production
2✔
43
        backend=default_backend(),
×
44
    )
45

46

47
@pytest.fixture(scope='module')
2✔
48
def rsa_public_key(rsa_private_key: RSAPrivateKey) -> RSAPublicKey:
2✔
49
    """Extract a public key out of the private one."""
50
    return rsa_private_key.public_key()
2✔
51

52

53
@pytest.fixture(scope='module')
2✔
54
def rsa_private_key_bytes(rsa_private_key: RSAPrivateKey) -> bytes:
2✔
55
    r"""Generate an unencrypted PKCS#1 formatted RSA private key.
56

57
    Encoded as PEM-bytes.
58

59
    This is what the GitHub-downloaded PEM files contain.
60

61
    Ref: https://developer.github.com/apps/building-github-apps/\
62
         authenticating-with-github-apps/
63
    """
64
    return rsa_private_key.private_bytes(
2✔
65
        encoding=Encoding.PEM,
2✔
66
        format=PrivateFormat.TraditionalOpenSSL,  # A.K.A. PKCS#1
2✔
67
        encryption_algorithm=NoEncryption(),
2✔
68
    )
69

70

71
@pytest.fixture(scope='module')
2✔
72
def rsa_private_key_str(rsa_private_key_bytes: bytes) -> str:
2✔
73
    """Return private key as an instance of string."""
74
    return rsa_private_key_bytes.decode('utf-8')
2✔
75

76

77
@pytest.fixture(scope='module')
2✔
78
def rsa_public_key_bytes(rsa_public_key: RSAPublicKey) -> bytes:
2✔
79
    """Return a PKCS#1 formatted RSA public key encoded as PEM."""
80
    return rsa_public_key.public_bytes(
2✔
81
        encoding=Encoding.PEM,
2✔
82
        format=PublicFormat.PKCS1,
2✔
83
    )
84

85

86
class AppInstallIds(TypedDict):
2✔
87
    """Schema for augmented extractor function keyword args."""
88

89
    app_or_client_id: str
2✔
90
    install_id: str
2✔
91

92

93
@pytest.mark.parametrize(
2✔
94
    (
95
        'github_exception',
2✔
96
        'transformed_exception',
2✔
97
        'error_msg',
2✔
98
    ),
99
    (
100
        (
101
            BadAttributeException(
×
102
                '',
2✔
103
                {},
104
                Exception(),
2✔
105
            ),
106
            RuntimeError,
2✔
107
            (
108
                r'^Broken GitHub @ https://github\.com with '
2✔
109
                r'app_or_client_id: 123, install_id: 456\. It is a bug, '
110
                'please report it to the '
111
                r"developers\.\n\n\('', \{\}, Exception\(\)\)$"
112
            ),
113
        ),
114
        (
115
            GithubException(-1),
2✔
116
            RuntimeError,
2✔
117
            (
118
                '^An unexpected error happened while talking to GitHub API '
2✔
119
                r'@ https://github\.com '
120
                r'\(app_or_client_id: 123, install_id: 456\)\. '
121
                r'Is the app or client ID correct\? '
122
                r'And the private RSA key\? '
123
                r'See https://docs\.github\.com/rest/reference/apps'
124
                r'#create-an-installation-access-token-for-an-app\.'
125
                r'\n\n-1$'
126
            ),
127
        ),
128
        (
129
            UnknownObjectException(-1),
2✔
130
            ValueError,
2✔
131
            (
132
                '^Failed to retrieve a GitHub installation token from '
2✔
133
                r'https://github\.com using '
134
                r'app_or_client_id: 123, install_id: 456\. '
135
                r'Is the app installed\? See '
136
                r'https://docs\.github\.com/rest/reference/apps'
137
                r'#create-an-installation-access-token-for-an-app\.'
138
                r'\n\n-1$'
139
            ),
140
        ),
141
    ),
142
    ids=(
143
        'github-broken',
2✔
144
        'unexpected-error',
2✔
145
        'no-install',
2✔
146
    ),
147
)
148
def test_github_app_api_errors(
2✔
149
    mocker: MockerFixture,
150
    github_exception: Exception,
151
    transformed_exception: type[Exception],
152
    error_msg: str,
153
) -> None:
154
    """Test successful GitHub authentication."""
155
    application_id = 123
2✔
156
    installation_id = 456
2✔
157

158
    mocker.patch.object(
×
159
        gh_app_plugin_mod.Auth.AppInstallationAuth,
2✔
160
        'token',
2✔
161
        new_callable=mocker.PropertyMock,
×
162
        side_effect=github_exception,
2✔
163
    )
164

165
    with pytest.raises(transformed_exception, match=error_msg):
2✔
166
        gh_app_plugin_mod.extract_github_app_install_token(
2✔
167
            github_api_url='https://github.com',
2✔
168
            app_or_client_id=application_id,
2✔
169
            install_id=installation_id,
2✔
170
            private_rsa_key='key',
2✔
171
        )
172

173

174
class _FakeAppInstallationAuth(AppInstallationAuth):
2✔
175
    @property
176
    def token(self: '_FakeAppInstallationAuth') -> str:
2✔
177
        return 'token-sentinel'
2✔
178

179

180
@pytest.mark.parametrize(
2✔
181
    'application_or_client_id',
2✔
182
    (
183
        pytest.param(TEST_APP_ID, id='app-id-int'),
2✔
184
        pytest.param(str(TEST_APP_ID), id='app-id-str'),
2✔
185
        pytest.param('Iv1.aaaaaaaaaaaaaaaa', id='client-id-iv1-legacy-format'),
2✔
186
        pytest.param('Iv2aaaaaaaaaaaaaaaa', id='client-id-iv2-new-format'),
2✔
187
        pytest.param(
2✔
188
            'Iv10aaaaaaaaaaaaaaaa',
2✔
189
            id='client-id-iv10-double-digit-version',
2✔
190
        ),
191
        pytest.param('Iv100.aaaaaaaaaaaaaaaa', id='client-id-iv100-with-dot'),
2✔
192
        pytest.param('Iv23likIfIXeZTb5GCAA', id='client-id-iv23-real-example'),
2✔
193
    ),
194
)
195
@pytest.mark.parametrize(
2✔
196
    'installation_id',
2✔
197
    (456, '456'),
2✔
198
    ids=('install-id-int', 'install-id-str'),
2✔
199
)
200
@pytest.mark.filterwarnings(
2✔
201
    'ignore:The RSA key is '  # intentional
2✔
202
    f'{MINIMUM_RSA_KEY_SIZE} bits long, which is below the minimum '
1✔
203
    'recommended size of 2048 bits. See NIST SP 800-131A.:'
204
    'jwt.warnings.InsecureKeyLengthWarning',
205
)
206
# pylint: disable-next=too-many-arguments,too-many-positional-arguments
207
def test_github_app_github_authentication(
2✔
208
    application_or_client_id: int | str,
209
    installation_id: int | str,
210
    mocker: MockerFixture,
211
    monkeypatch: pytest.MonkeyPatch,
212
    rsa_private_key_str: str,
213
    rsa_public_key_bytes: bytes,
214
) -> None:
215
    """Test successful GitHub authentication."""
216
    monkeypatch.setattr(
2✔
217
        gh_app_plugin_mod.Auth,
2✔
218
        'AppInstallationAuth',
2✔
219
        _FakeAppInstallationAuth,
2✔
220
    )
221

222
    get_installation_auth_spy = mocker.spy(
2✔
223
        gh_app_plugin_mod.Auth,
2✔
224
        'AppInstallationAuth',
2✔
225
    )
226
    github_initializer_spy = mocker.spy(gh_app_plugin_mod, 'Github')
2✔
227

228
    token = gh_app_plugin_mod.extract_github_app_install_token(
2✔
229
        github_api_url='https://github.com',
2✔
230
        app_or_client_id=application_or_client_id,
2✔
231
        install_id=installation_id,
2✔
232
        private_rsa_key=rsa_private_key_str,
2✔
233
    )
234

235
    observed_pygithub_obj = github_initializer_spy.spy_return
×
236
    observed_gh_install_auth_obj = get_installation_auth_spy.spy_return
×
237
    # pylint: disable-next=protected-access
NEW
238
    signed_jwt = observed_gh_install_auth_obj._app_auth.token
×
239

240
    assert token == 'token-sentinel'
2✔
241

242
    assert observed_pygithub_obj.requester.base_url == 'https://github.com'
×
243

244
    assert observed_gh_install_auth_obj.installation_id == int(installation_id)
×
245
    assert isinstance(observed_gh_install_auth_obj, _FakeAppInstallationAuth)
×
246

247
    # NOTE: The `decode_jwt()` call asserts that no
248
    # NOTE: `jwt.exceptions.InvalidSignatureError()` exception gets raised
249
    # NOTE: which would indicate incorrect RSA key or corrupted payload if
250
    # NOTE: that was to happen. This verifies that JWT is signed with the
251
    # NOTE: private RSA key we passed by using its public counterpart.
252
    decode_jwt(
2✔
253
        signed_jwt,
×
254
        key=rsa_public_key_bytes,
2✔
255
        algorithms=[DEFAULT_JWT_ALGORITHM],
2✔
256
        options={
257
            'require': ['exp', 'iat', 'iss'],
2✔
258
            'strict_aud': False,
2✔
259
            'verify_aud': True,
2✔
260
            'verify_exp': True,
2✔
261
            'verify_signature': True,
2✔
262
            'verify_nbf': True,
2✔
263
        },
264
        audience=None,  # GH App JWT don't set the audience claim
2✔
265
        issuer=str(application_or_client_id),
2✔
266
        leeway=0.001,
2✔
267
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc