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

commit 3fb4007b8179c044b64f212b6f38b5d37a4c5ed2
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Apr 7 15:21:19 2026 +0100

    Add tests for JS serialisation of multiple file uploads
---
 tests/e2e/upload/__init__.py   |  16 ++++++
 tests/e2e/upload/conftest.py   |  61 ++++++++++++++++++++++
 tests/e2e/upload/test_post.py  |  69 +++++++++++++++++++++++++
 tests/unit/test_upload_json.py | 114 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 260 insertions(+)

diff --git a/tests/e2e/upload/__init__.py b/tests/e2e/upload/__init__.py
new file mode 100644
index 00000000..13a83393
--- /dev/null
+++ b/tests/e2e/upload/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/tests/e2e/upload/conftest.py b/tests/e2e/upload/conftest.py
new file mode 100644
index 00000000..5c8175f6
--- /dev/null
+++ b/tests/e2e/upload/conftest.py
@@ -0,0 +1,61 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Final
+
+import e2e.helpers as helpers
+import pytest
+
+if TYPE_CHECKING:
+    from collections.abc import Generator
+
+    from playwright.sync_api import Browser, BrowserContext, Page
+
+PROJECT_KEY: Final[str] = "test"
+VERSION_KEY: Final[str] = "0.1+e2e-upload"
+
+
[email protected]
+def page_upload(upload_context: BrowserContext) -> Generator[Page]:
+    """Navigate to the upload page with a fresh page for each test."""
+    page = upload_context.new_page()
+    helpers.visit(page, f"/upload/{PROJECT_KEY}/{VERSION_KEY}")
+    yield page
+    page.close()
+
+
[email protected](scope="module")
+def upload_context(browser: Browser) -> Generator[BrowserContext]:
+    """Create a fresh release ready for upload testing."""
+    context = browser.new_context(ignore_https_errors=True)
+    page = context.new_page()
+
+    helpers.log_in(page)
+    helpers.delete_release_if_exists(page, PROJECT_KEY, VERSION_KEY)
+
+    helpers.visit(page, f"/start/{PROJECT_KEY}")
+    page.locator("input#version_key").fill(VERSION_KEY)
+    page.get_by_role("button", name="Start new release").click()
+    page.wait_for_url(f"**/compose/{PROJECT_KEY}/{VERSION_KEY}")
+
+    page.close()
+
+    yield context
+
+    context.close()
diff --git a/tests/e2e/upload/test_post.py b/tests/e2e/upload/test_post.py
new file mode 100644
index 00000000..2e5db193
--- /dev/null
+++ b/tests/e2e/upload/test_post.py
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import time
+from typing import Final
+
+import e2e.helpers as helpers
+from playwright.sync_api import Page, Request, Route, expect
+
+PROJECT_KEY: Final[str] = "test"
+VERSION_KEY: Final[str] = "0.1+e2e-upload"
+COMPOSE_URL: Final[str] = f"/compose/{PROJECT_KEY}/{VERSION_KEY}"
+
+
+def test_multi_file_upload(page_upload: Page) -> None:
+    """Two files uploaded in one submission produce a single revision."""
+    page = page_upload
+
+    upload_posts: list[str] = []
+    page.on("request", lambda req: _record_upload_post(req, upload_posts))
+
+    page.locator('input[name="file_data"]').set_input_files(
+        [
+            {"name": "NOTICE.txt", "mimeType": "text/plain", "buffer": 
b"Apache Notice"},
+            {"name": "README.txt", "mimeType": "text/plain", "buffer": b"Read 
me"},
+        ]
+    )
+
+    page.route("**/upload/test/**", _delay_post_response)
+    page.get_by_role("button", name="Add files").click()
+    
expect(page.locator("#upload-progress-container")).to_be_visible(timeout=5000)
+    page.unroute("**/upload/test/**")
+
+    page.wait_for_url(f"**{COMPOSE_URL}", timeout=30000)
+
+    assert len(upload_posts) == 1
+    assert f"/upload/{PROJECT_KEY}/" in upload_posts[0]
+
+    helpers.wait_for_upload_and_tasks(page, COMPOSE_URL, "NOTICE.txt", 
timeout=60)
+    files_table = page.locator("#files-table-container")
+    expect(files_table.get_by_role("cell", name="README.txt", 
exact=True)).to_be_visible()
+
+    helpers.visit(page, f"/revisions/{PROJECT_KEY}/{VERSION_KEY}")
+    expect(page.locator(".card.mb-3")).to_have_count(2)
+
+
+def _delay_post_response(route: Route) -> None:
+    if route.request.method == "POST":
+        time.sleep(1)
+    route.continue_()
+
+
+def _record_upload_post(request: Request, upload_posts: list[str]) -> None:
+    if request.method == "POST" and "/upload/" in request.url:
+        upload_posts.append(request.url)
diff --git a/tests/unit/test_upload_json.py b/tests/unit/test_upload_json.py
new file mode 100644
index 00000000..9a4e31c8
--- /dev/null
+++ b/tests/unit/test_upload_json.py
@@ -0,0 +1,114 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+import unittest.mock as mock
+
+import pytest
+import quart
+
+import atr.post.upload as upload
+
+
[email protected]
+def app():
+    app = quart.Quart(__name__)
+    app.secret_key = "test"
+    app.config["TESTING"] = True
+    return app
+
+
[email protected]
+async def test_add_files_html_redirect_on_success(app):
+    redirect_response = mock.MagicMock()
+    session = mock.AsyncMock()
+    session.redirect = mock.AsyncMock(return_value=redirect_response)
+
+    patched = _mock_storage(None, 2, False)
+
+    with mock.patch.object(upload, "storage", patched):
+        async with app.test_request_context("/upload/test/1.0"):
+            result = await upload._add_files(
+                session,
+                mock.MagicMock(),
+                mock.MagicMock(),
+                mock.MagicMock(),
+                wants_json=False,
+            )
+
+    assert result is redirect_response
+    session.redirect.assert_called_once()
+
+
[email protected]
+async def test_add_files_json_creation_error(app):
+    patched = _mock_storage("No files provided", 0, False)
+
+    with mock.patch.object(upload, "storage", patched):
+        async with app.test_request_context("/upload/test/1.0"):
+            result = await upload._add_files(
+                mock.AsyncMock(),
+                mock.MagicMock(),
+                mock.MagicMock(),
+                mock.MagicMock(),
+                wants_json=True,
+            )
+
+    response, status = result
+    assert status == 400
+    data = json.loads(await response.data)
+    assert data["ok"] is False
+    assert data["message"] == "No files provided"
+
+
[email protected]
+async def test_add_files_json_success(app):
+    patched = _mock_storage(None, 2, False)
+
+    with (
+        mock.patch.object(upload, "storage", patched),
+        mock.patch.object(upload.util, "as_url", 
return_value="/compose/test/1.0"),
+    ):
+        async with app.test_request_context("/upload/test/1.0"):
+            result = await upload._add_files(
+                mock.AsyncMock(),
+                mock.MagicMock(),
+                mock.MagicMock(),
+                mock.MagicMock(),
+                wants_json=True,
+            )
+
+    response, status = result
+    assert status == 200
+    data = json.loads(await response.data)
+    assert data["ok"] is True
+    assert data["next_url"] == "/compose/test/1.0"
+    assert "2 files" in data["message"]
+
+
+def _mock_storage(creation_error, number_of_files, was_quarantined):
+    wacp = mock.AsyncMock()
+    wacp.release.upload_files = mock.AsyncMock(
+        return_value=(creation_error, number_of_files, was_quarantined),
+    )
+    write = mock.AsyncMock()
+    write.as_project_committee_participant = mock.AsyncMock(return_value=wacp)
+
+    cm = mock.AsyncMock()
+    cm.__aenter__.return_value = write
+
+    return mock.MagicMock(write=mock.MagicMock(return_value=cm))


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to