From ed50f06bfe235fe0cf6f916a17da58c5ff816662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1zgar=20Ozan?= Date: Sat, 3 Oct 2026 12:53:50 +0300 Subject: [PATCH] [Confluence] Server attach_content: always POST to update an attachment The update branch only used POST when api_version was "1.0", but Server now defaults to "latest", so updating an existing attachment went to the PUT path that Server doesn't support. Fixes #1681 --- atlassian/confluence/server/__init__.py | 28 +++++++--------------- tests/confluence/test_confluence_server.py | 17 +++++++++++++ 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/atlassian/confluence/server/__init__.py b/atlassian/confluence/server/__init__.py index 1a4cce5e0..676c32bf3 100644 --- a/atlassian/confluence/server/__init__.py +++ b/atlassian/confluence/server/__init__.py @@ -1437,26 +1437,16 @@ def attach_content( raise if existing_attachment: - # Update existing attachment on the specific attachment ID + # Update existing attachment on the specific attachment ID. Server/Data Center + # only accepts POST on the data path below the child, whatever api_version is set. attachment_id = existing_attachment["id"] - if self.api_version == "1.0": - # older API versions use POST on data path below the child - update_path = f"{path}/{attachment_id}/data" - response = self.post( - path=update_path, - data=data, - headers=headers, - files={"file": (name, content, content_type)}, - ) - else: - # newer API versions use PUT on a path derived from the attachment ID directly - update_path = f"rest/api/content/{attachment_id}" - response = self.put( - path=update_path, - data=data, - headers=headers, - files={"file": (name, content, content_type)}, - ) + update_path = f"{path}/{attachment_id}/data" + response = self.post( + path=update_path, + data=data, + headers=headers, + files={"file": (name, content, content_type)}, + ) else: # Create new attachment using POST response = self.post( diff --git a/tests/confluence/test_confluence_server.py b/tests/confluence/test_confluence_server.py index 7c9a52693..8eb2174fb 100644 --- a/tests/confluence/test_confluence_server.py +++ b/tests/confluence/test_confluence_server.py @@ -190,6 +190,23 @@ def test_attach_content_normalizes_path_like_attachment_names(self, mock_post, m assert mock_post.call_args.kwargs["files"] == {"file": ("diagram.png", content, "image/png")} + @pytest.mark.parametrize("api_version", [None, "latest", "1.0"], ids=["default", "latest", "1.0"]) + @patch.object(ConfluenceServer, "put") + @patch.object(ConfluenceServer, "get") + @patch.object(ConfluenceServer, "post") + def test_attach_content_updates_existing_attachment_with_post(self, mock_post, mock_get, mock_put, api_version): + kwargs = {} if api_version is None else {"api_version": api_version} + confluence = ConfluenceServer(url="https://test.confluence.com", username="test", password="test", **kwargs) + content = io.BytesIO(b"new image") + mock_get.return_value = {"results": [{"id": "att-1", "title": "diagram.png"}]} + mock_post.return_value = {"id": "att-1"} + + confluence.attach_content(content, "diagram.png", "image/png", page_id="123") + + mock_put.assert_not_called() + assert mock_post.call_args.kwargs["path"] == "rest/api/content/123/child/attachment/att-1/data" + assert mock_post.call_args.kwargs["files"] == {"file": ("diagram.png", content, "image/png")} + def test_attach_content_rejects_empty_attachment_name(self, confluence_server): with pytest.raises(ApiValueError, match="must contain a filename"): confluence_server.attach_content(io.BytesIO(b"content"), "/", page_id="123")