Repository: andreroggeri/pynubank

Issue:
Problemas com função _save_auth_data no arquivo nubank.py
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/sergio/Documentos/pynubank/pynubank/nubank.py", line 99, in authenticate_with_cert
    self._save_auth_data(response)
  File "/home/sergio/Documentos/pynubank/pynubank/nubank.py", line 63, in _save_auth_data
    self.feed_url = auth_data['_links']['events']['href']
KeyError: 'events'

Este erro ocorre ao tentar autenticar. Precisei corrigir a função _save_auth_data do arquivo nubank.py substituindo os nomes de chaves que mudaram no request. A função corrigida fica assim:
def _save_auth_data(self, auth_data: dict) -> None:
        self.client.set_header('Authorization', f'Bearer {auth_data["access_token"]}')
        self.feed_url = auth_data['_links']['magnitude']['href']
        self.query_url = auth_data['_links']['ghostflame']['href']
        self.bills_url = auth_data['_links']['savings_account']['href']



Comments:

Patch:
diff --git a/pynubank/exception.py b/pynubank/exception.py
index 4326696..c5bc410 100644
--- a/pynubank/exception.py
+++ b/pynubank/exception.py
@@ -7,6 +7,11 @@ class NuException(Exception):
         super().__init__(message)
 
 
+class NuMissingCreditCard(NuException):
+    def __init__(self):
+        super().__init__(f'Couldn\'t fetch bills due to missing credit card.')
+
+
 class NuRequestException(NuException):
     def __init__(self, response: Response):
         super().__init__(f'The request made failed with HTTP status code {response.status_code}')
diff --git a/pynubank/nubank.py b/pynubank/nubank.py
index 4ed2338..f83fd98 100644
--- a/pynubank/nubank.py
+++ b/pynubank/nubank.py
@@ -6,6 +6,7 @@ from qrcode import QRCode
 from pynubank.utils.discovery import Discovery
 from pynubank.utils.http import HttpClient
 from pynubank.utils.graphql import prepare_request_body
+from pynubank.exception import NuMissingCreditCard
 
 PAYMENT_EVENT_TYPES = (
     'TransferOutEvent',
@@ -43,11 +44,23 @@ class Nubank:
         }
         return self.client.post(self.discovery.get_url('login'), json=payload)
 
+    def _find_url(self, known_keys: set, links: dict) -> str:
+            intersection = known_keys.intersection(links)
+            iterator = iter(intersection)
+            key = next(iterator, None)
+            return links.get(key, {}).get('href', None)
+
     def _save_auth_data(self, auth_data: dict) -> None:
         self.client.set_header('Authorization', f'Bearer {auth_data["access_token"]}')
-        self.feed_url = auth_data['_links']['events']['href']
-        self.query_url = auth_data['_links']['ghostflame']['href']
-        self.bills_url = auth_data['_links']['bills_summary']['href']
+
+        links = auth_data['_links']
+        self.query_url = links['ghostflame']['href']
+
+        feed_url_keys = {'magnitude', 'events'}
+        bills_url_keys = {'bills_summary'}
+
+        self.feed_url = self._find_url(feed_url_keys, links)
+        self.bills_url = self._find_url(bills_url_keys, links)
 
     def get_qr_code(self) -> Tuple[str, QRCode]:
         content = str(uuid.uuid4())
@@ -108,8 +121,11 @@ class Nubank:
         return list(filter(lambda x: x['category'] == 'transaction', feed['events']))
 
     def get_bills(self):
-        request = self.client.get(self.bills_url)
-        return request['bills']
+        if self.bills_url is not None:
+            request = self.client.get(self.bills_url)
+            return request['bills']
+        else:
+            raise NuMissingCreditCard
 
     def get_bill_details(self, bill: dict):
         return self.client.get(bill['_links']['self']['href'])
diff --git a/pynubank/utils/mock_http.py b/pynubank/utils/mock_http.py
index 19c5f30..b4ae558 100644
--- a/pynubank/utils/mock_http.py
+++ b/pynubank/utils/mock_http.py
@@ -33,6 +33,12 @@ class MockHttpClient(HttpClient):
         self._results[('https://mocked-proxy-url/api/proxy/ghostflame_123',
                        str(prepare_request_body('create_money_request')))] = self._read_data('money')
 
+    def add_mock_url(self, url: str, graphql_object: str, response_json_name: str):
+        self._results[(url, graphql_object)] = self._read_data(response_json_name)
+
+    def remove_mock_url(self, route: tuple):
+        del self._results[route]
+
     def get(self, url: str) -> dict:
         result = self._find(url)
         if result is None:
diff --git a/pynubank/utils/mocked_responses/discovery_login_alternative.json b/pynubank/utils/mocked_responses/discovery_login_alternative.json
new file mode 100644
index 0000000..986c4c8
--- /dev/null
+++ b/pynubank/utils/mocked_responses/discovery_login_alternative.json
@@ -0,0 +1,51 @@
+{
+  "access_token": "access_token_123",
+  "token_type": "bearer",
+  "_links": {
+    "change_password": {
+      "href": "https://mocked-proxy-url/api/proxy/change_password_123"
+    },
+    "enabled_features": {
+      "href": "https://mocked-proxy-url/api/proxy/enabled_features_123"
+    },
+    "revoke_token": {
+      "href": "https://mocked-proxy-url/api/proxy/revoke_token_123"
+    },
+    "userinfo": {
+      "href": "https://mocked-proxy-url/api/proxy/userinfo_123"
+    },
+    "events_page": {
+      "href": "https://mocked-proxy-url/api/proxy/events_page_123"
+    },
+    "postcode": {
+      "href": "https://mocked-proxy-url/api/proxy/post_code_123"
+    },
+    "app_flows": {
+      "href": "https://mocked-proxy-url/api/proxy/app_flows_123"
+    },
+    "revoke_all": {
+      "href": "https://mocked-proxy-url/api/proxy/revoke_all_123"
+    },
+    "customer": {
+      "href": "https://mocked-proxy-url/api/proxy/customer_123"
+    },
+    "account": {
+      "href": "https://mocked-proxy-url/api/proxy/account_123"
+    },
+    "purchases": {
+      "href": "https://mocked-proxy-url/api/proxy/purchases_123"
+    },
+    "ghostflame": {
+      "href": "https://mocked-proxy-url/api/proxy/ghostflame_123"
+    },
+    "user_change_password": {
+      "href": "https://mocked-proxy-url/api/proxy/user_change_password_123"
+    },
+    "magnitude": {
+      "href": "https://mocked-proxy-url/api/proxy/magnitude_123"
+    },
+    "savings_account": {
+      "href": "https://mocked-proxy-url/api/proxy/savings_account_123"
+    }
+  }
+}


Test patch:
diff --git a/requirements-test.txt b/requirements-test.txt
index 038e63b..467289f 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,5 +1,5 @@
 -r requirements.txt
-pytest==6.1.1
+pytest==6.1.2
 pytest-cov==2.10.1
 coveralls==2.1.2
 tox==3.20.1
diff --git a/tests/test_nubank_client.py b/tests/test_nubank_client.py
index 2072af9..d4dddd8 100644
--- a/tests/test_nubank_client.py
+++ b/tests/test_nubank_client.py
@@ -1,10 +1,8 @@
-from unittest.mock import MagicMock, Mock
-
+import pytest
 from qrcode import QRCode
-
 from pynubank.nubank import Nubank
-from pynubank.utils.http import HttpClient
 from pynubank import MockHttpClient
+from pynubank.exception import NuMissingCreditCard
 
 
 def test_authenticate_with_qr_code_succeeds():
@@ -31,6 +29,22 @@ def test_authenticate_with_refresh_token():
     assert nubank_client.client.get_header('Authorization') == 'Bearer access_token_123'
 
 
+def test_authenticate_with_cert_missing_credit_card():
+    mock_client = MockHttpClient()
+    mock_client.remove_mock_url(('https://mocked-proxy-url/api/proxy/events_123', ''))
+    mock_client.remove_mock_url(('https://mocked-proxy-url/api/token', ''))
+
+    mock_client.add_mock_url('https://mocked-proxy-url/api/proxy/magnitude_123', '', 'proxy_events')
+    mock_client.add_mock_url('https://mocked-proxy-url/api/token', '', 'discovery_login_alternative')
+
+    nubank_client = Nubank(client=mock_client)
+    nubank_client.authenticate_with_cert('1234', 'hunter12', 'some-file.p12')
+
+    assert nubank_client.feed_url == 'https://mocked-proxy-url/api/proxy/magnitude_123'
+    assert nubank_client.bills_url is None
+    assert nubank_client.client.get_header('Authorization') == 'Bearer access_token_123'
+
+
 def test_get_card_feed():
     nubank_client = Nubank(client=MockHttpClient())
     nubank_client.authenticate_with_qr_code('12345678912', 'hunter12', 'some-uuid')
@@ -56,6 +70,21 @@ def test_get_card_feed():
     assert events[0]['_links']['self']['href'] == 'https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/_links_123'
 
 
+def test_get_bills_missing_credit_card():
+    mock_client = MockHttpClient()
+    mock_client.remove_mock_url(('https://mocked-proxy-url/api/proxy/events_123', ''))
+    mock_client.remove_mock_url(('https://mocked-proxy-url/api/token', ''))
+
+    mock_client.add_mock_url('https://mocked-proxy-url/api/proxy/magnitude_123', '', 'proxy_events')
+    mock_client.add_mock_url('https://mocked-proxy-url/api/token', '', 'discovery_login_alternative')
+
+    nubank_client = Nubank(client=mock_client)
+    nubank_client.authenticate_with_cert('1234', 'hunter12', 'some-file.p12')
+
+    with pytest.raises(NuMissingCreditCard):
+        nubank_client.get_bills()
+
+
 def test_get_bills():
     nubank_client = Nubank(client=MockHttpClient())
     nubank_client.authenticate_with_qr_code('12345678912', 'hunter12', 'some-uuid')