You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/08/11 17:38:20 UTC

(tooling-trusted-releases) branch sbp updated: Authenticate the SVN export and list reads

This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/sbp by this push:
     new 4c91cafb Authenticate the SVN export and list reads
4c91cafb is described below

commit 4c91cafbeb5e8661ae3a38dcedd5a11a9088d767
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Tue Aug 11 18:37:50 2026 +0100

    Authenticate the SVN export and list reads
---
 atr/svn/__init__.py           | 16 ++++++++++--
 tests/unit/test_svn_export.py | 61 ++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 74 insertions(+), 3 deletions(-)

diff --git a/atr/svn/__init__.py b/atr/svn/__init__.py
index 34fcec64..f9ea542f 100644
--- a/atr/svn/__init__.py
+++ b/atr/svn/__init__.py
@@ -181,14 +181,16 @@ async def export(
     destination: pathlib.Path,
     timeout_seconds: float = EXPORT_TIMEOUT_SECONDS,
 ) -> None:
+    stdin_bytes = _authentication(url)
     arguments = ["export", "--non-interactive", "--ignore-externals", "--ignore-keywords"]
     if revision is not None:
         arguments.extend(["-r", str(revision)])
         pegged_url = f"{url}@{revision}"
     else:
         pegged_url = f"{url}@"
+    arguments.extend(["--username", ASF_TOOL, "--password-from-stdin"])
     arguments.extend(["--", pegged_url, str(destination)])
-    await run_command("svn", *arguments, timeout_seconds=timeout_seconds)
+    await run_command("svn", *arguments, timeout_seconds=timeout_seconds, stdin_bytes=stdin_bytes)
 
 
 async def get_diff(path: pathlib.Path, revision: int) -> str:
@@ -245,7 +247,17 @@ async def info_authenticated(url: str) -> str:
 
 async def list_files(url: str) -> list[str]:
     """List every file below a URL, as paths relative to it. Directories are left out."""
-    output = await _run_svn_command("list", url, "--recursive", timeout_seconds=LIST_TIMEOUT_SECONDS)
+    output = await _run_svn_command(
+        "list",
+        url,
+        "--recursive",
+        "--username",
+        ASF_TOOL,
+        "--password-from-stdin",
+        "--non-interactive",
+        timeout_seconds=LIST_TIMEOUT_SECONDS,
+        stdin_bytes=_authentication(url),
+    )
     return [line for line in output.splitlines() if line and (not line.endswith("/"))]
 
 
diff --git a/tests/unit/test_svn_export.py b/tests/unit/test_svn_export.py
index 1622f605..439fa6a7 100644
--- a/tests/unit/test_svn_export.py
+++ b/tests/unit/test_svn_export.py
@@ -21,6 +21,7 @@ import unittest.mock as mock
 
 import pytest
 
+import atr.config as config
 import atr.svn as svn
 
 requires_svn = pytest.mark.skipif(
@@ -30,8 +31,10 @@ requires_svn = pytest.mark.skipif(
 
 
 @requires_svn
-async def test_export_pinned_revision_of_deleted_path(tmp_path: pathlib.Path) -> None:
+async def test_export_pinned_revision_of_deleted_path(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(config.get(), "SVN_TOKEN", "dummy", raising=False)
     repository = tmp_path / "repository"
+    monkeypatch.setattr(config.get(), "SVN_PUBLISH_URL", f"file://{repository}", raising=False)
     await svn.run_command("svnadmin", "create", str(repository))
     url = f"file://{repository}/release"
     source = tmp_path / "source"
@@ -47,6 +50,8 @@ async def test_export_pinned_revision_of_deleted_path(tmp_path: pathlib.Path) ->
 
 
 async def test_export_pins_revision(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(config.get(), "SVN_TOKEN", "dummy", raising=False)
+    monkeypatch.setattr(config.get(), "SVN_PUBLISH_URL", "file:///repo", raising=False)
     run_command = mock.AsyncMock(return_value="")
     monkeypatch.setattr(svn, "run_command", run_command)
     destination = tmp_path / "dest"
@@ -61,14 +66,31 @@ async def test_export_pins_revision(tmp_path: pathlib.Path, monkeypatch: pytest.
         "--ignore-keywords",
         "-r",
         "42",
+        "--username",
+        svn.ASF_TOOL,
+        "--password-from-stdin",
         "--",
         "file:///repo/example@42",
         str(destination),
         timeout_seconds=svn.EXPORT_TIMEOUT_SECONDS,
+        stdin_bytes=b"dummy",
     )
 
 
+async def test_export_requires_token(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(config.get(), "SVN_TOKEN", None, raising=False)
+    run_command = mock.AsyncMock(return_value="")
+    monkeypatch.setattr(svn, "run_command", run_command)
+
+    with pytest.raises(ValueError, match="SVN_TOKEN"):
+        await svn.export("file:///repo/example", 1, tmp_path / "dest")
+
+    run_command.assert_not_awaited()
+
+
 async def test_export_without_revision(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(config.get(), "SVN_TOKEN", "dummy", raising=False)
+    monkeypatch.setattr(config.get(), "SVN_PUBLISH_URL", "file:///repo", raising=False)
     run_command = mock.AsyncMock(return_value="")
     monkeypatch.setattr(svn, "run_command", run_command)
     destination = tmp_path / "dest"
@@ -81,8 +103,45 @@ async def test_export_without_revision(tmp_path: pathlib.Path, monkeypatch: pyte
         "--non-interactive",
         "--ignore-externals",
         "--ignore-keywords",
+        "--username",
+        svn.ASF_TOOL,
+        "--password-from-stdin",
         "--",
         "file:///repo/example@",
         str(destination),
         timeout_seconds=10.0,
+        stdin_bytes=b"dummy",
     )
+
+
+async def test_list_files_authenticates(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(config.get(), "SVN_TOKEN", "dummy", raising=False)
+    monkeypatch.setattr(config.get(), "SVN_PUBLISH_URL", "file:///repo", raising=False)
+    run_command = mock.AsyncMock(return_value="a.tar.gz\nsub/\nsub/b.tar.gz\n")
+    monkeypatch.setattr(svn, "run_command", run_command)
+
+    assert await svn.list_files("file:///repo/example") == ["a.tar.gz", "sub/b.tar.gz"]
+
+    run_command.assert_awaited_once_with(
+        "svn",
+        "list",
+        "--recursive",
+        "--username",
+        svn.ASF_TOOL,
+        "--password-from-stdin",
+        "--non-interactive",
+        "file:///repo/example",
+        timeout_seconds=svn.LIST_TIMEOUT_SECONDS,
+        stdin_bytes=b"dummy",
+    )
+
+
+async def test_list_files_requires_token(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(config.get(), "SVN_TOKEN", None, raising=False)
+    run_command = mock.AsyncMock(return_value="")
+    monkeypatch.setattr(svn, "run_command", run_command)
+
+    with pytest.raises(ValueError, match="SVN_TOKEN"):
+        await svn.list_files("file:///repo/example")
+
+    run_command.assert_not_awaited()


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org