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]
