Files
orisys-sdk/tests/test_release_auth.py
2026-09-12 17:34:46 +08:00

388 lines
14 KiB
Python

"""Tests for PyPI credential setup (~/.pypirc handling).
The writer does text surgery to preserve the explanatory comments in the file, so
these tests check the result of that surgery rather than trusting it. No test
touches the real ``~/.pypirc``: every path is injected.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
BUILD_ROOT = REPO_ROOT / "build"
if str(BUILD_ROOT) not in sys.path:
sys.path.insert(0, str(BUILD_ROOT))
from release import auth # noqa: E402
TOKEN = "pypi-" + "A" * 30
COMMENTED_TEMPLATE = """\
# keep me: why this file exists
# second comment line
[distutils]
index-servers =
pypi
[pypi]
repository = https://upload.pypi.org/legacy/
username = __token__
password = pypi-PASTE-YOUR-PRODUCTION-TOKEN-HERE
# [testpypi] commented out on purpose
# password = pypi-OLD
"""
def test_parse_pypirc_reads_sections_and_ignores_comments():
sections = auth.parse_pypirc(COMMENTED_TEMPLATE)
assert sections["pypi"]["username"] == "__token__"
assert "testpypi" not in sections, "commented-out sections must stay inactive"
def test_placeholder_is_detected_as_not_configured(tmp_path):
path = tmp_path / ".pypirc"
path.write_text(COMMENTED_TEMPLATE, encoding="utf-8")
state = auth.credential_state("pypi", path=path, env={})
assert not state.configured
assert "placeholder" in state.problem
def test_missing_file_is_reported(tmp_path):
state = auth.credential_state("pypi", path=tmp_path / "nope", env={})
assert not state.configured
assert "does not exist" in state.problem
def test_env_variables_count_as_configured(tmp_path):
state = auth.credential_state(
"pypi", path=tmp_path / "nope", env={"TWINE_PASSWORD": "pypi-x"}
)
assert state.configured
assert state.source == "environment variables"
def test_wrong_username_is_rejected(tmp_path):
path = tmp_path / ".pypirc"
path.write_text(
"[pypi]\nusername = someone@example.com\npassword = pypi-real-token-value\n",
encoding="utf-8",
)
state = auth.credential_state("pypi", path=path, env={})
assert not state.configured
assert "__token__" in state.problem
def test_write_token_replaces_placeholder_and_keeps_comments(tmp_path):
path = tmp_path / ".pypirc"
path.write_text(COMMENTED_TEMPLATE, encoding="utf-8")
auth.write_token("pypi", TOKEN, path=path)
text = path.read_text(encoding="utf-8")
assert "# keep me: why this file exists" in text
assert "# [testpypi] commented out on purpose" in text
assert f"password = {TOKEN}" in text
assert "PASTE-YOUR-PRODUCTION-TOKEN-HERE" not in text
assert text.count("[pypi]") == 1
assert auth.credential_state("pypi", path=path, env={}).configured
def test_write_token_adds_missing_section_and_index_servers(tmp_path):
path = tmp_path / ".pypirc"
path.write_text(COMMENTED_TEMPLATE, encoding="utf-8")
auth.write_token("testpypi", TOKEN, path=path)
text = path.read_text(encoding="utf-8")
assert "[testpypi]" in text
assert auth.index_servers(text) == {"pypi", "testpypi"}
assert auth.credential_state("testpypi", path=path, env={}).configured
# The production entry must be untouched.
assert "PASTE-YOUR-PRODUCTION-TOKEN-HERE" in text
def test_write_token_creates_file_when_absent(tmp_path):
path = tmp_path / "nested" / ".pypirc"
auth.write_token("pypi", TOKEN, path=path)
assert path.is_file()
text = path.read_text(encoding="utf-8")
assert "index-servers" in text
assert auth.credential_state("pypi", path=path, env={}).configured
def test_write_token_updates_existing_password_in_place(tmp_path):
path = tmp_path / ".pypirc"
auth.write_token("pypi", TOKEN, path=path)
replacement = "pypi-" + "B" * 30
auth.write_token("pypi", replacement, path=path)
text = path.read_text(encoding="utf-8")
assert f"password = {replacement}" in text
assert TOKEN not in text
assert text.count("password") == 1
def test_upsert_token_does_not_leak_into_the_next_section():
text = "[pypi]\nusername = __token__\n\n[testpypi]\nusername = __token__\n"
result = auth.upsert_token(text, "pypi", TOKEN)
pypi_block, testpypi_block = result.split("[testpypi]")
assert TOKEN in pypi_block
assert TOKEN not in testpypi_block
def test_validate_token_rejects_empty_and_whitespace():
assert auth.validate_token("")
assert auth.validate_token("pypi-abc def")
assert not auth.validate_token(TOKEN)
def test_token_warnings_flag_suspicious_pastes():
assert any("pypi-" in warning for warning in auth.token_warnings("not-a-token"))
assert any("too short" in warning for warning in auth.token_warnings("pypi-abc"))
assert auth.token_warnings(TOKEN) == []
def test_prompt_never_echoes_the_token(capsys):
token = auth.prompt_for_token("pypi", prompt_fn=lambda label: TOKEN)
captured = capsys.readouterr()
assert token == TOKEN
assert TOKEN not in captured.out
assert "hidden" in captured.out
def test_prompt_abort_returns_none():
def _raise(label):
raise EOFError
assert auth.prompt_for_token("pypi", prompt_fn=_raise) is None
def test_ensure_credentials_prompts_when_interactive(tmp_path, monkeypatch, capsys):
from release import pypi
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
monkeypatch.setattr(auth, "prompt_for_token", lambda index, **k: TOKEN)
# _ensure_credentials resolves the path through auth.credential_state, so the
# monkeypatched pypirc_path applies there too.
options = pypi.Options(version="1.2.3", apply=True)
assert pypi._ensure_credentials(["pypi"], options, interactive=True) is True
assert "[ok ] wrote [pypi] credentials" in capsys.readouterr().out
assert auth.credential_state("pypi", path=tmp_path / ".pypirc", env={}).configured
def test_ensure_credentials_fails_without_a_terminal(tmp_path, monkeypatch, capsys):
from release import pypi
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
options = pypi.Options(version="1.2.3", apply=True)
assert pypi._ensure_credentials(["pypi"], options, interactive=False) is False
output = capsys.readouterr().out
assert "--set-token pypi" in output
assert "prompted" in output
assert not (tmp_path / ".pypirc").exists(), "must not write anything when it cannot prompt"
def test_ensure_credentials_dry_run_warns_but_continues(tmp_path, monkeypatch, capsys):
from release import pypi
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
options = pypi.Options(version="1.2.3", apply=False)
assert pypi._ensure_credentials(["pypi"], options, interactive=False) is True
output = capsys.readouterr().out
assert "[warn]" in output
assert "dry run continues" in output
def test_ensure_credentials_reports_testpypi_alternative(tmp_path, monkeypatch, capsys):
from release import pypi
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
options = pypi.Options(version="1.2.3", apply=True)
assert pypi._ensure_credentials(["testpypi"], options, interactive=False) is False
output = capsys.readouterr().out
assert "--set-token testpypi" in output
assert "--pypi-only" in output
def test_piped_token_is_used_only_once(tmp_path, monkeypatch, capsys):
from release import pypi
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
options = pypi.Options(version="1.2.3", apply=True, token=TOKEN)
assert pypi._ensure_credentials(["pypi", "testpypi"], options, interactive=True) is False
# Second index had no token left to consume, so it fell back to the prompt,
# which is unavailable in tests; nothing may be written for it.
assert options.token is None
assert auth.credential_state("pypi", path=tmp_path / ".pypirc", env={}).configured
assert not auth.credential_state(
"testpypi", path=tmp_path / ".pypirc", env={}
).configured
def test_section_not_listed_in_index_servers_is_not_configured(tmp_path):
"""twine only honours sections named in [distutils] index-servers.
Regression test for a real failure: a hand-added [testpypi] section looked
configured to us while twine answered "Missing 'testpypi' section".
"""
path = tmp_path / ".pypirc"
path.write_text(
"[distutils]\n"
"index-servers =\n"
" pypi\n"
"\n"
"[pypi]\n"
"username = __token__\n"
"password = pypi-" + "A" * 30 + "\n"
"\n"
"[testpypi]\n"
"username = __token__\n"
"password = pypi-" + "B" * 30 + "\n",
encoding="utf-8",
)
state = auth.credential_state("testpypi", path=path, env={})
assert not state.configured
assert "index-servers" in state.problem
assert "--repair" in state.problem
# The production index is unaffected.
assert auth.credential_state("pypi", path=path, env={}).configured
def test_credential_state_agrees_with_twine(tmp_path):
"""Our verdict must match twine's own resolver for the same file."""
twine_utils = pytest.importorskip("twine.utils")
path = tmp_path / ".pypirc"
path.write_text(
"[distutils]\n"
"index-servers =\n"
" pypi\n"
"\n"
"[pypi]\n"
"username = __token__\n"
"password = pypi-" + "A" * 30 + "\n"
"\n"
"[testpypi]\n"
"username = __token__\n"
"password = pypi-" + "B" * 30 + "\n",
encoding="utf-8",
)
twine_sees = set(twine_utils.get_config(str(path)))
for index in ("pypi", "testpypi"):
ours = auth.credential_state(index, path=path, env={})
# twine is the authority: we may only claim configured when twine agrees.
if ours.configured:
assert index in twine_sees, f"{index} accepted by us but ignored by twine"
def test_missing_index_servers_key_falls_back_to_both_indexes(tmp_path):
"""twine's fallback is 'pypi testpypi' only when the key is absent entirely."""
path = tmp_path / ".pypirc"
text = "[distutils]\nindex-servers =\n pypi\n\n[ pypi ]\n".replace("[ pypi ]", "[pypi]")
path.write_text(text, encoding="utf-8")
assert auth.index_servers(path.read_text(encoding="utf-8")) == {"pypi"}
without_key = path.read_text(encoding="utf-8").replace("index-servers =\n pypi\n", "")
path.write_text(without_key, encoding="utf-8", newline="")
assert auth.index_servers(path.read_text(encoding="utf-8")) == {"pypi", "testpypi"}
def test_empty_index_servers_honours_nothing(tmp_path):
"""A trap: `index-servers =` with no entries means twine honours no sections.
Unlike a missing key, an empty value does not trigger the pypi+testpypi
fallback, so a section that looks configured is ignored.
"""
path = tmp_path / ".pypirc"
path.write_text(
"[distutils]\nindex-servers =\n\n"
"[pypi]\nusername = __token__\npassword = pypi-" + "A" * 30 + "\n",
encoding="utf-8",
)
assert auth.index_servers(path.read_text(encoding="utf-8")) == set()
state = auth.credential_state("pypi", path=path, env={})
assert not state.configured
assert "index-servers" in state.problem
auth.repair_index_servers(path)
assert auth.credential_state("pypi", path=path, env={}).configured
def test_index_servers_reads_inline_and_continuation_forms():
assert auth.index_servers("[distutils]\nindex-servers = pypi testpypi\n") == {
"pypi",
"testpypi",
}
assert auth.index_servers("[distutils]\nindex-servers =\n pypi\n") == {"pypi"}
assert auth.index_servers("") == {"pypi", "testpypi"}
def test_unlisted_sections_reports_the_gap(tmp_path):
path = tmp_path / ".pypirc"
path.write_text(
"[distutils]\nindex-servers =\n pypi\n\n"
"[pypi]\npassword = pypi-x\n\n"
"[testpypi]\npassword = pypi-y\n",
encoding="utf-8",
)
assert auth.unlisted_sections(path) == ["testpypi"]
def test_repair_index_servers_fixes_a_hand_edited_file(tmp_path):
path = tmp_path / ".pypirc"
path.write_text(
"[distutils]\nindex-servers =\n pypi\n\n"
"[pypi]\nusername = __token__\npassword = pypi-" + "A" * 30 + "\n\n"
"[testpypi]\nusername = __token__\npassword = pypi-" + "B" * 30 + "\n",
encoding="utf-8",
)
assert not auth.credential_state("testpypi", path=path, env={}).configured
added = auth.repair_index_servers(path)
assert added == ["testpypi"]
assert auth.index_servers(path.read_text(encoding="utf-8")) == {"pypi", "testpypi"}
assert auth.credential_state("testpypi", path=path, env={}).configured
# Tokens and comments survive the repair.
assert "pypi-" + "B" * 30 in path.read_text(encoding="utf-8")
def test_repair_index_servers_is_a_no_op_when_correct(tmp_path):
path = tmp_path / ".pypirc"
auth.write_token("pypi", "pypi-" + "A" * 30, path=path)
before = path.read_text(encoding="utf-8")
assert auth.repair_index_servers(path) == []
assert path.read_text(encoding="utf-8") == before
def test_write_token_makes_the_result_visible_to_twine(tmp_path):
"""The writer must produce a file twine actually honours, not just one we parse."""
path = tmp_path / ".pypirc"
path.write_text("[distutils]\nindex-servers =\n pypi\n", encoding="utf-8")
auth.write_token("testpypi", "pypi-" + "B" * 30, path=path)
assert auth.credential_state("testpypi", path=path, env={}).configured
twine_utils = pytest.importorskip("twine.utils")
assert "testpypi" in set(twine_utils.get_config(str(path)))
@pytest.mark.parametrize("index", ["pypi", "testpypi"])
def test_write_then_state_round_trips(tmp_path, index):
path = tmp_path / ".pypirc"
auth.write_token(index, TOKEN, path=path)
state = auth.credential_state(index, path=path, env={})
assert state.configured, state.problem