Repository: dixudx/rtcclient

Issue:
url not found: jazz/authenticated
Hi,
I was trying to use your plugin to, at least, connect to a jazz server using your `get_workitem.py` script into `examples/how_to/workitem` folder.

I've changed the file with : 

```python
    url = "https://__the_url__:443/ccm/jazz"
    username = "my_username"
    password = "my_password"
    myclient = RTCClient(url, username, password, ends_with_jazz=True)
```

Adding also `ends_with_jazz=True` that I saw from docs/other issues.

This is the trace I get:

```python
2023-03-22 14:49:42,753 DEBUG client.RTCClient: Get response from https://__the_url__:443/ccm/jazz/authenticated/identity
2023-03-22 14:49:42,764 DEBUG urllib3.connectionpool: Starting new HTTPS connection (1): __the_url__:443
2023-03-22 14:49:42,979 DEBUG urllib3.connectionpool: https://__the_url__:443 "GET /ccm/jazz/authenticated/identity HTTP/1.1" 302 0
2023-03-22 14:49:43,000 DEBUG urllib3.connectionpool: Resetting dropped connection: __the_url__
2023-03-22 14:49:43,153 DEBUG urllib3.connectionpool: https://__the_url__:443 "GET /ccm/secure/authenticated/identity?redirectPath=%2Fccm%2Fjazz%2Fauthenticated%2Fidentity HTTP/1.1" 302 0
2023-03-22 14:49:43,167 DEBUG urllib3.connectionpool: Resetting dropped connection: __the_url__
2023-03-22 14:49:43,325 DEBUG urllib3.connectionpool: https://__the_url__:443 "GET /ccm/auth/authrequired HTTP/1.1" 200 1072
2023-03-22 14:49:48,130 DEBUG client.RTCClient: Get response from https://__the_url__:443/ccm/jazz/authenticated/identity
2023-03-22 14:49:48,141 DEBUG urllib3.connectionpool: Starting new HTTPS connection (1): __the_url__:443
2023-03-22 14:49:48,368 DEBUG urllib3.connectionpool: https://__the_url__:443 "GET /ccm/jazz/authenticated/identity HTTP/1.1" 302 0
2023-03-22 14:49:48,381 DEBUG urllib3.connectionpool: Resetting dropped connection: __the_url__
2023-03-22 14:49:48,543 DEBUG urllib3.connectionpool: https://__the_url__:443 "GET /ccm/secure/authenticated/identity?redirectPath=%2Fccm%2Fjazz%2Fauthenticated%2Fidentity HTTP/1.1" 302 0
2023-03-22 14:49:48,583 DEBUG urllib3.connectionpool: Resetting dropped connection: __the_url__
2023-03-22 14:49:48,837 DEBUG urllib3.connectionpool: https://__the_url__:443 "GET /ccm/auth/authrequired HTTP/1.1" 200 1072
2023-03-22 14:49:56,708 DEBUG client.RTCClient: Post a request to https://__the_url__:443/ccm/jazz/authenticated/j_security_check with data: {'j_username': 'my_username', 'j_password': 'my_password'} and json: None
2023-03-22 14:49:56,717 DEBUG urllib3.connectionpool: Starting new HTTPS connection (1): __the_url__:443
2023-03-22 14:49:57,598 DEBUG urllib3.connectionpool: https://__the_url__:443 "POST /ccm/jazz/authenticated/j_security_check HTTP/1.1" 302 0
2023-03-22 14:49:57,610 DEBUG urllib3.connectionpool: Resetting dropped connection: __the_url__
2023-03-22 14:49:57,768 DEBUG urllib3.connectionpool: https://__the_url__:443 "GET /ccm/jazz/authenticated/ HTTP/1.1" 404 None
2023-03-22 14:49:57,773 ERROR client.RTCClient: Failed POST request at <https://__the_url__:443/ccm/jazz/authenticated/j_security_check> with response: b'Error 404: Not Found\n'
2023-03-22 14:49:57,773 INFO client.RTCClient: 404
Traceback (most recent call last):

  File "...\rtcclient\examples\how_to\workitem\get_workitem.py", line 13, in <module>
    myclient = RTCClient(url, username, password, ends_with_jazz=True)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...\rtcclient\rtcclient\client.py", line 77, in __init__
    self.headers = self._get_headers()
                   ^^^^^^^^^^^^^^^^^^^
  File "...\rtcclient\rtcclient\client.py", line 155, in _get_headers
    resp = self.post(self.url + "/authenticated/j_security_check",
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...\rtcclient\rtcclient\utils.py", line 27, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "...\rtcclient\rtcclient\base.py", line 144, in post
    response.raise_for_status()
  File "...\rtcclient\venv\Lib\site-packages\requests\models.py", line 1021, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://__the_url__/ccm/jazz/authenticated/
python-BaseException
```


So the point that raises exception is [this line](https://github.com/dixudx/rtcclient/blob/master/rtcclient/client.py#L136), the line number differs because of some tests I made related to cookies (from a fork), but those changes are commented.



Some information on my environment:

- Python3.11
- rtcclient checkout of main branch
- IBM Jazz 7.0.2


Thank you!

Comments:

Patch:
diff --git a/examples/how_to/workitem/get_workitem.py b/examples/how_to/workitem/get_workitem.py
index ef095f8..7064f40 100644
--- a/examples/how_to/workitem/get_workitem.py
+++ b/examples/how_to/workitem/get_workitem.py
@@ -9,7 +9,7 @@ if __name__ == "__main__":
     url = "https://your_domain:9443/jazz"
     username = "your_username"
     password = "your_password"
-    myclient = RTCClient(url, username, password)
+    myclient = RTCClient(url, username, password)  # ends_with_jazz , old_rtc_authentication kwargs
 
     # get all workitems
     # If both projectarea_id and projectarea_name are None, all the workitems
diff --git a/poetry.lock b/poetry.lock
index 739b595..7f9ad3d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -747,6 +747,18 @@ files = [
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
 
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+
 [[package]]
 name = "tomli"
 version = "2.0.1"
@@ -932,4 +944,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
         python-versions = ">=3.7.0,<4.0"
-content-hash = "e34c2dbb238bb43211cc83b01a6fef54cfed2547b3f034dd4c7e47940cc84f95"
+content-hash = "d1b93aabe50ca296f8324af41db0ba3b97b09444730854b7ebc3497f6e70fbdb"
diff --git a/pyproject.toml b/pyproject.toml
index bd2a89d..7fe23fe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -53,6 +53,7 @@
             ]
             yapf = "*"
             tox = "*"
+            toml = "*"
 
 
 [build-system]
diff --git a/rtcclient/client.py b/rtcclient/client.py
index aba7afb..5d2be51 100644
--- a/rtcclient/client.py
+++ b/rtcclient/client.py
@@ -8,7 +8,7 @@ import six
 import xmltodict
 
 from rtcclient import exception
-from rtcclient import urlparse, urlquote, OrderedDict
+from rtcclient import urlencode, urlparse, urlquote, OrderedDict
 from rtcclient.base import RTCBase
 from rtcclient.models import FiledAgainst, FoundIn, Comment, Action, State  # noqa: F401
 from rtcclient.models import IncludedInBuild, ChangeSet, Attachment  # noqa: F401
@@ -58,7 +58,8 @@ class RTCClient(RTCBase):
                  proxies=None,
                  searchpath=None,
                  ends_with_jazz=True,
-                 verify: Union[bool, str] = False):
+                 verify: Union[bool, str] = False,
+                 old_rtc_authentication=False):
         """Initialization
 
         See params above
@@ -68,6 +69,7 @@ class RTCClient(RTCBase):
         self.password = password
         self.proxies = proxies
         self.verify = verify
+        self.old_rtc_authentication = old_rtc_authentication
         RTCBase.__init__(self, url)
 
         if not isinstance(ends_with_jazz, bool):
@@ -99,6 +101,24 @@ class RTCClient(RTCBase):
                         proxies=self.proxies,
                         allow_redirects=_allow_redirects)
 
+        if self.old_rtc_authentication:
+            # works with server that needs 0.6.0 version
+            _headers["Content-Type"] = self.CONTENT_URL_ENCODED
+            if resp.headers.get("set-cookie") is not None:
+                _headers["Cookie"] = resp.headers.get("set-cookie")
+
+            credentials = urlencode({
+                "j_username": self.username,
+                "j_password": self.password
+            })
+
+            resp = self.post(self.url + "/authenticated/j_security_check",
+                             data=credentials,
+                             verify=False,
+                             headers=_headers,
+                             proxies=self.proxies,
+                             allow_redirects=_allow_redirects)
+
         # authfailed
         authfailed = resp.headers.get("x-com-ibm-team-repository-web-auth-msg")
         if authfailed == "authfailed":


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index ad6309f..49e7fd5 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -2,6 +2,8 @@ from rtcclient import RTCClient
 import requests
 import pytest
 import utils_test
+from unittest.mock import call
+
 from rtcclient.project_area import ProjectArea
 from rtcclient.models import Severity, Priority, FoundIn, FiledAgainst
 from rtcclient.models import TeamArea, Member, PlannedFor
@@ -42,6 +44,254 @@ def test_headers(mocker):
     assert client.headers == expected_headers
 
 
+def test_client_rest_calls_new_auth(mocker):
+    mocked_get = mocker.patch("requests.get")
+    mocked_post = mocker.patch("requests.post")
+
+    mock_rsp = mocker.MagicMock(spec=requests.Response)
+    mock_rsp.status_code = 200
+
+    mocked_get.return_value = mock_rsp
+    mocked_post.return_value = mock_rsp
+
+    mock_rsp.headers = {"set-cookie": "cookie-id"}
+    _ = RTCClient(url="http://test.url:9443/jazz",
+                  username="user",
+                  password="password")
+
+    # assert GET calls
+    assert mocked_get.call_count == 2
+    mocked_get.assert_has_calls([
+        call('http://test.url:9443/jazz/authenticated/identity',
+             verify=False,
+             headers={
+                 'Content-Type': 'text/xml',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             auth=('user', 'password'),
+             allow_redirects=True),
+        call('http://test.url:9443/jazz/authenticated/identity',
+             verify=False,
+             headers={
+                 'Content-Type': 'text/xml',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             auth=('user', 'password'),
+             allow_redirects=True)
+    ])
+
+    # assert POST calls
+    assert mocked_post.call_count == 0
+
+
+def test_client_rest_calls_old_auth(mocker):
+    mocked_get = mocker.patch("requests.get")
+    mocked_post = mocker.patch("requests.post")
+
+    mock_rsp = mocker.MagicMock(spec=requests.Response)
+    mock_rsp.status_code = 200
+
+    mocked_get.return_value = mock_rsp
+    mocked_post.return_value = mock_rsp
+
+    mock_rsp.headers = {"set-cookie": "cookie-id"}
+    _ = RTCClient(url="http://test.url:9443/jazz",
+                  username="user",
+                  password="password",
+                  old_rtc_authentication=True)
+
+    # assert GET calls
+    assert mocked_get.call_count == 2
+    mocked_get.assert_has_calls([
+        call('http://test.url:9443/jazz/authenticated/identity',
+             verify=False,
+             headers={
+                 'Content-Type': 'application/x-www-form-urlencoded',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             auth=('user', 'password'),
+             allow_redirects=True),
+        call('http://test.url:9443/jazz/authenticated/identity',
+             verify=False,
+             headers={
+                 'Content-Type': 'application/x-www-form-urlencoded',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             auth=('user', 'password'),
+             allow_redirects=True)
+    ])
+
+    # assert POST calls
+    assert mocked_post.call_count == 1
+    mocked_post.assert_has_calls([
+        call('http://test.url:9443/jazz/authenticated/j_security_check',
+             data='j_username=user&j_password=password',
+             json=None,
+             verify=False,
+             headers={
+                 'Content-Type': 'application/x-www-form-urlencoded',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             allow_redirects=True)
+    ])
+
+
+def test_headers_auth_required_new_auth(mocker):
+    mocked_get = mocker.patch("requests.get")
+    mocked_post = mocker.patch("requests.post")
+
+    mock_rsp = mocker.MagicMock(spec=requests.Response)
+    mock_rsp.status_code = 200
+
+    # auth required
+    mock_rsp.headers = {
+        "set-cookie": "cookie-id",
+        "X-com-ibm-team-repository-web-auth-msg": "authrequired"
+    }
+
+    mocked_get.return_value = mock_rsp
+    mocked_post.return_value = mock_rsp
+
+    _ = RTCClient(url="http://test.url:9443/jazz",
+                  username="user",
+                  password="password")
+
+    # assert GET calls
+    assert mocked_get.call_count == 2
+    mocked_get.assert_has_calls([
+        call('http://test.url:9443/jazz/authenticated/identity',
+             verify=False,
+             headers={
+                 'Content-Type': 'text/xml',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             auth=('user', 'password'),
+             allow_redirects=True),
+        call('http://test.url:9443/jazz/authenticated/identity',
+             verify=False,
+             headers={
+                 'Content-Type': 'text/xml',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             auth=('user', 'password'),
+             allow_redirects=True)
+    ])
+
+    # assert POST calls
+    assert mocked_post.call_count == 1
+    mocked_post.assert_has_calls([
+        call('http://test.url:9443/jazz/authenticated/j_security_check',
+             data={
+                 'j_username': 'user',
+                 'j_password': 'password'
+             },
+             json=None,
+             verify=False,
+             headers={'Content-Type': 'application/x-www-form-urlencoded'},
+             proxies=None,
+             timeout=60,
+             allow_redirects=True)
+    ])
+
+
+def test_headers_auth_required_old_auth(mocker):
+    mocked_get = mocker.patch("requests.get")
+    mocked_post = mocker.patch("requests.post")
+
+    mock_rsp = mocker.MagicMock(spec=requests.Response)
+    mock_rsp.status_code = 200
+
+    # auth required
+    mock_rsp.headers = {
+        "set-cookie": "cookie-id",
+        "X-com-ibm-team-repository-web-auth-msg": "authrequired"
+    }
+
+    mocked_get.return_value = mock_rsp
+    mocked_post.return_value = mock_rsp
+
+    _ = RTCClient(url="http://test.url:9443/jazz",
+                  username="user",
+                  password="password",
+                  old_rtc_authentication=True)
+
+    # assert GET calls
+    assert mocked_get.call_count == 2
+    mocked_get.assert_has_calls([
+        call('http://test.url:9443/jazz/authenticated/identity',
+             verify=False,
+             headers={
+                 'Content-Type': 'application/x-www-form-urlencoded',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             auth=('user', 'password'),
+             allow_redirects=True),
+        call('http://test.url:9443/jazz/authenticated/identity',
+             verify=False,
+             headers={
+                 'Content-Type': 'application/x-www-form-urlencoded',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             auth=('user', 'password'),
+             allow_redirects=True)
+    ])
+
+    # assert POST calls
+    assert mocked_post.call_count == 2
+    mocked_post.assert_has_calls([
+        call('http://test.url:9443/jazz/authenticated/j_security_check',
+             data='j_username=user&j_password=password',
+             json=None,
+             verify=False,
+             headers={
+                 'Content-Type': 'application/x-www-form-urlencoded',
+                 'Cookie': 'cookie-id',
+                 'Accept': 'text/xml'
+             },
+             proxies=None,
+             timeout=60,
+             allow_redirects=True),
+        call('http://test.url:9443/jazz/authenticated/j_security_check',
+             data={
+                 'j_username': 'user',
+                 'j_password': 'password'
+             },
+             json=None,
+             verify=False,
+             headers={'Content-Type': 'application/x-www-form-urlencoded'},
+             proxies=None,
+             timeout=60,
+             allow_redirects=True)
+    ])
+
+
 class TestRTCClient:
 
     @pytest.fixture(autouse=True)