Skip to content

Commit 454c708

Browse files
committed
fixup! WIP: Automatically replace Trusted Publishing Tokens
1 parent f4cd53d commit 454c708

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ exclude_lines =
1313

1414
# exclude typing.TYPE_CHECKING
1515
if TYPE_CHECKING:
16+
if t.TYPE_CHECKING:
1617

1718
[html]
1819
show_contexts = True

tests/test_auth.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import base64
12
import getpass
23
import logging
34
import platform
45
import re
6+
import time
7+
import typing as t
58

69
import pytest
10+
import requests.auth
711

812
from twine import auth
913
from twine import exceptions
@@ -299,3 +303,154 @@ def test_warns_for_empty_password(
299303
)
300304
def test_keyring_module():
301305
assert auth.keyring is not None
306+
307+
308+
def test_resolver_authenticator_config_authentication(config):
309+
config.update(username="username", password="password")
310+
res = auth.Resolver(config, auth.CredentialInput())
311+
assert isinstance(res.authenticator, requests.auth.HTTPBasicAuth)
312+
313+
314+
def test_resolver_authenticator_credential_input_authentication(config):
315+
res = auth.Resolver(config, auth.CredentialInput("username", "password"))
316+
assert isinstance(res.authenticator, requests.auth.HTTPBasicAuth)
317+
318+
319+
def test_resolver_authenticator_trusted_publishing_authentication(config):
320+
res = auth.Resolver(
321+
config, auth.CredentialInput(username="__token__", password="skip-stdin")
322+
)
323+
res._tp_token = auth.TrustedPublishingToken(
324+
success=True,
325+
token="fake-tp-token",
326+
)
327+
assert isinstance(res.authenticator, auth.TrustedPublishingAuthenticator)
328+
329+
330+
class MockResponse:
331+
def __init__(self, status_code: int, json: t.Any) -> None:
332+
self.status_code = status_code
333+
self._json = json
334+
335+
def json(self, *args, **kwargs) -> t.Any:
336+
return self._json
337+
338+
def raise_for_status(self) -> None:
339+
if 400 <= self.status_code:
340+
raise requests.exceptions.HTTPError()
341+
342+
def ok(self) -> bool:
343+
return self.status_code == 200
344+
345+
346+
class MockSession:
347+
def __init__(
348+
self,
349+
get_response_list: t.List[MockResponse],
350+
post_response_list: t.List[MockResponse],
351+
) -> None:
352+
self.post_counter = self.get_counter = 0
353+
self.get_response_list = get_response_list
354+
self.post_response_list = post_response_list
355+
356+
def get(self, url: str, **kwargs) -> MockResponse:
357+
response = self.get_response_list[self.get_counter]
358+
self.get_counter += 1
359+
return response
360+
361+
def post(self, url: str, **kwargs) -> MockResponse:
362+
response = self.post_response_list[self.post_counter]
363+
self.post_counter += 1
364+
return response
365+
366+
367+
def test_trusted_publish_authenticator_refreshes_token(monkeypatch, config):
368+
def make_session():
369+
return MockSession(
370+
get_response_list=[
371+
MockResponse(status_code=200, json={"audience": "fake-aud"})
372+
],
373+
post_response_list=[
374+
MockResponse(
375+
status_code=200,
376+
json={
377+
"success": True,
378+
"token": "new-token",
379+
"expires": int(time.time()) + 900,
380+
},
381+
),
382+
],
383+
)
384+
385+
def detect_credential(*args, **kwargs) -> str:
386+
return "fake-oidc-token"
387+
388+
config.update({"repository": utils.TEST_REPOSITORY})
389+
res = auth.Resolver(config, auth.CredentialInput(username="__token__"))
390+
res._tp_token = auth.TrustedPublishingToken(
391+
success=True,
392+
token="expiring-tp-token",
393+
)
394+
res._expires = int(time.time()) + 4 * 60
395+
monkeypatch.setattr(auth, "detect_credential", detect_credential)
396+
monkeypatch.setattr(auth.utils, "make_requests_session", make_session)
397+
authenticator = auth.TrustedPublishingAuthenticator(resolver=res)
398+
prepped_req = requests.models.PreparedRequest()
399+
prepped_req.prepare_headers({})
400+
request = authenticator(prepped_req)
401+
assert (
402+
request.headers["Authorization"]
403+
== f"Basic {base64.b64encode(b'__token__:new-token').decode()}"
404+
)
405+
406+
407+
def test_trusted_publish_authenticator_reuses_token(monkeypatch, config):
408+
def make_session():
409+
return MockSession(
410+
get_response_list=[
411+
MockResponse(status_code=200, json={"audience": "fake-aud"})
412+
],
413+
post_response_list=[
414+
MockResponse(
415+
status_code=200,
416+
json={
417+
"success": True,
418+
"token": "new-token",
419+
"expires": int(time.time()) + 900,
420+
},
421+
),
422+
],
423+
)
424+
425+
def detect_credential(*args, **kwargs) -> str:
426+
return "fake-oidc-token"
427+
428+
config.update({"repository": utils.TEST_REPOSITORY})
429+
res = auth.Resolver(config, auth.CredentialInput(username="__token__"))
430+
res._tp_token = auth.TrustedPublishingToken(
431+
success=True,
432+
token="valid-tp-token",
433+
)
434+
res._expires = int(time.time()) + 900
435+
monkeypatch.setattr(auth, "detect_credential", detect_credential)
436+
monkeypatch.setattr(auth.utils, "make_requests_session", make_session)
437+
authenticator = auth.TrustedPublishingAuthenticator(resolver=res)
438+
prepped_req = requests.models.PreparedRequest()
439+
prepped_req.prepare_headers({})
440+
request = authenticator(prepped_req)
441+
assert (
442+
request.headers["Authorization"]
443+
== f"Basic {base64.b64encode(b'__token__:valid-tp-token').decode()}"
444+
)
445+
446+
447+
def test_inability_to_make_token_raises_error():
448+
class MockResolver:
449+
def make_trusted_publishing_token(self) -> None:
450+
return None
451+
452+
authenticator = auth.TrustedPublishingAuthenticator(
453+
resolver=MockResolver(),
454+
)
455+
with pytest.raises(exceptions.TrustedPublishingFailure):
456+
authenticator(None)

0 commit comments

Comments
 (0)