Skip to content

Commit cd2108c

Browse files
committed
Preserve legacy Confluence Server request paths
1 parent 996ea93 commit cd2108c

2 files changed

Lines changed: 138 additions & 2 deletions

File tree

‎atlassian/confluence/server/__init__.py‎

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ class Server(ConfluenceServerBase):
4242
Confluence Server REST API wrapper
4343
"""
4444

45+
_api_resources = frozenset(
46+
{
47+
"accessmode",
48+
"admin",
49+
"audit",
50+
"content",
51+
"contentbody",
52+
"group",
53+
"health",
54+
"longtask",
55+
"metadata",
56+
"reindex",
57+
"search",
58+
"space",
59+
"template",
60+
"user",
61+
}
62+
)
63+
4564
content_types = {
4665
".gif": "image/gif",
4766
".png": "image/png",
@@ -54,15 +73,64 @@ class Server(ConfluenceServerBase):
5473
}
5574

5675
def __init__(self, url, *args, **kwargs):
76+
api_version_is_explicit = "api_version" in kwargs
5777
# Set default values only if not provided
5878
if "cloud" not in kwargs:
5979
kwargs["cloud"] = False
6080
if "api_version" not in kwargs:
6181
kwargs["api_version"] = "1.0"
6282
if "api_root" not in kwargs:
6383
kwargs["api_root"] = "rest/api"
64-
url = url.strip("/") + f"/{kwargs['api_root']}/{kwargs['api_version']}"
65-
super(Server, self).__init__(url, *args, **kwargs)
84+
super(Server, self).__init__(url.rstrip("/"), *args, **kwargs)
85+
self._api_version_is_explicit = api_version_is_explicit
86+
87+
def _server_api_path(self, path):
88+
"""Prefix unrooted Server REST resources without changing legacy paths."""
89+
if not isinstance(path, str):
90+
return path
91+
92+
normalized_path = path.lstrip("/")
93+
resource = normalized_path.split("/", 1)[0].split("?", 1)[0]
94+
if resource not in self._api_resources:
95+
return path
96+
97+
api_parts = [self.api_root]
98+
if self._api_version_is_explicit:
99+
api_parts.append(self.api_version)
100+
api_parts.append(normalized_path)
101+
return "/".join(str(part).strip("/") for part in api_parts if part is not None and str(part).strip("/"))
102+
103+
def request(
104+
self,
105+
method="GET",
106+
path="/",
107+
data=None,
108+
json=None,
109+
flags=None,
110+
params=None,
111+
headers=None,
112+
files=None,
113+
trailing=None,
114+
absolute=False,
115+
advanced_mode=False,
116+
allow_redirects=True,
117+
):
118+
if not absolute:
119+
path = self._server_api_path(path)
120+
return super(Server, self).request(
121+
method=method,
122+
path=path,
123+
data=data,
124+
json=json,
125+
flags=flags,
126+
params=params,
127+
headers=headers,
128+
files=files,
129+
trailing=trailing,
130+
absolute=absolute,
131+
advanced_mode=advanced_mode,
132+
allow_redirects=allow_redirects,
133+
)
66134

67135
@staticmethod
68136
def _create_body(body, representation):

‎tests/confluence/test_confluence_server.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def test_init_defaults(self):
3030
assert confluence.api_version == "1.0"
3131
assert confluence.api_root == "rest/api"
3232
assert confluence.cloud is False
33+
assert confluence.url == "https://test.confluence.com"
3334

3435
def test_init_custom_values(self):
3536
"""Test ConfluenceServer client initialization with custom values."""
@@ -43,6 +44,73 @@ def test_init_custom_values(self):
4344
assert confluence.api_version == "2.0"
4445
assert confluence.api_root == "custom/api/root"
4546

47+
def test_default_server_requests_preserve_legacy_urls(self):
48+
confluence = ConfluenceServer(url="https://test.confluence.com", token="test-token")
49+
response = Response()
50+
response.status_code = 200
51+
response.reason = "OK"
52+
response._content = b'{"id": "123"}'
53+
54+
with patch.object(confluence._session, "request", return_value=response) as mock_request:
55+
assert confluence.get_page_by_id("123") == {"id": "123"}
56+
57+
assert mock_request.call_args.kwargs["url"] == "https://test.confluence.com/rest/api/content/123"
58+
59+
@pytest.mark.parametrize(
60+
("method", "path", "kwargs", "expected_path"),
61+
[
62+
("post", "rest/api/content", {"data": {}}, "rest/api/content"),
63+
("put", "/rest/api/content/123", {"data": {}}, "rest/api/content/123"),
64+
("delete", "rest/api/content/123", {"params": {}}, "rest/api/content/123"),
65+
],
66+
)
67+
def test_legacy_rooted_request_paths_are_not_prefixed_twice(self, method, path, kwargs, expected_path):
68+
confluence = ConfluenceServer(url="https://test.confluence.com", token="test-token")
69+
response = Response()
70+
response.status_code = 200
71+
response.reason = "OK"
72+
73+
with patch.object(confluence._session, "request", return_value=response) as mock_request:
74+
getattr(confluence, method)(path=path, advanced_mode=True, **kwargs)
75+
76+
assert mock_request.call_args.kwargs["url"] == f"https://test.confluence.com/{expected_path}"
77+
78+
def test_explicit_server_api_version_applies_to_unrooted_resources(self):
79+
confluence = ConfluenceServer(
80+
url="https://test.confluence.com",
81+
token="test-token",
82+
api_root="custom/api/root",
83+
api_version="2.0",
84+
)
85+
response = Response()
86+
response.status_code = 200
87+
response.reason = "OK"
88+
response._content = b'{"id": "123"}'
89+
90+
with patch.object(confluence._session, "request", return_value=response) as mock_request:
91+
assert confluence.get_page_by_id("123") == {"id": "123"}
92+
93+
assert mock_request.call_args.kwargs["url"] == "https://test.confluence.com/custom/api/root/2.0/content/123"
94+
95+
def test_server_ui_exports_remain_site_relative(self):
96+
confluence = ConfluenceServer(url="https://test.confluence.com", token="test-token")
97+
response = Response()
98+
response.status_code = 200
99+
response.reason = "OK"
100+
response._content = b"%PDF-1.4"
101+
102+
with patch.object(confluence._session, "request", return_value=response) as mock_request:
103+
assert confluence.get_page_as_pdf("123") == b"%PDF-1.4"
104+
pdf_url = mock_request.call_args.kwargs["url"]
105+
106+
mock_request.reset_mock()
107+
response._content = b"word export"
108+
assert confluence.get_page_as_word("123") == b"word export"
109+
word_url = mock_request.call_args.kwargs["url"]
110+
111+
assert pdf_url == "https://test.confluence.com/spaces/flyingpdf/pdfpageexport.action?pageId=123"
112+
assert word_url == "https://test.confluence.com/exportword?pageId=123"
113+
46114
def test_bad_request_includes_confluence_validation_details(self, confluence_server):
47115
response = Response()
48116
response.status_code = 400

0 commit comments

Comments
 (0)