From 78ef8ec752a3a796440578be886ddbd643295ca0 Mon Sep 17 00:00:00 2001 From: Lucas Rangel Cezimbra Date: Tue, 22 Aug 2023 10:11:51 -0300 Subject: [PATCH] Fix credit card statements & support multiple cards (#209) * fixed credit card statements & support multiple cards * Add test fo AuthenticatedHome.menu_op * Add tests for AuthenticatedHomePage.menu_op * Add Menu2Page * Add tests for CardDetails page * Fix tests get_credit_card_invoice --------- Co-authored-by: Ivan Neto --- pyitau/main.py | 49 ++- pyitau/pages.py | 39 +- tests/conftest.py | 77 +++- tests/pages/test_authenticated_home.py | 25 +- tests/pages/test_card_details.py | 18 +- tests/pages/test_menu.py | 4 - tests/pages/test_menu2.py | 22 ++ tests/responses/card_details.html | 514 +++++++++++++++++++++++-- tests/test_get_credit_card_invoice.py | 79 ++-- 9 files changed, 711 insertions(+), 116 deletions(-) create mode 100644 tests/pages/test_menu2.py diff --git a/pyitau/main.py b/pyitau/main.py index 241878f..862e2fc 100644 --- a/pyitau/main.py +++ b/pyitau/main.py @@ -1,11 +1,11 @@ import requests from cached_property import cached_property -from pyitau.pages import (AuthenticatedHomePage, CardDetails, CardsPage, +from pyitau.pages import (AuthenticatedHomePage, CardDetails, CheckingAccountFullStatement, CheckingAccountMenu, - CheckingAccountStatementsPage, CheckingCardsMenu, - FirstRouterPage, MenuPage, PasswordPage, - SecondRouterPage, ThirdRouterPage) + CheckingAccountStatementsPage, FirstRouterPage, + Menu2Page, MenuPage, PasswordPage, SecondRouterPage, + ThirdRouterPage) ROUTER_URL = 'https://internetpf5.itau.com.br/router-app/router' @@ -37,26 +37,45 @@ def authenticate(self): self._authenticate8() self._authenticate9() - def get_credit_card_invoice(self): + def get_credit_card_invoice(self, card_name=None): """ Get and return the credit card invoice. """ + self._session.post(ROUTER_URL, headers={"op": self._home.op, "segmento": "VAREJO"}) + + response = self._session.post(ROUTER_URL, headers={"op": self._home.menu_op}) + # TODO: is it possible to use only Menu2Page and remove MenuPage? + menu = Menu2Page(response.text) + + response = self._session.post(ROUTER_URL, headers={ + "op": menu.checking_cards_op, + "X-FLOW-ID": self._flow_id, + "X-CLIENT-ID": self._client_id, + "X-Requested-With": "XMLHttpRequest", + }) + card_details = CardDetails(response.text) + response = self._session.post( ROUTER_URL, - headers={'op': self._menu_page.checking_cards_op} + headers={"op": card_details.invoice_op}, + data={"secao": "Cartoes", "item": "Home"}, ) + cards = response.json()["object"]["data"] - cards_menu = CheckingCardsMenu(response.text) - response = self._session.post(ROUTER_URL, headers={'op': cards_menu.cards_op}) + self._session.post( + ROUTER_URL, + headers={"op": card_details.invoice_op}, + data={"secao": "Cartoes:MinhaFatura", "item": ""}, + ) - cards_page = CardsPage(response.text) - response = self._session.post(ROUTER_URL, headers={'op': cards_page.card_details_op}, - data={'idCartao': cards_page.first_card_id}) + if not card_name: + card_id = cards[0]['id'] + else: + card_id = next(c for c in cards if c['nome'] == card_name)['id'] - card_details = CardDetails(response.text) - response = self._session.post(ROUTER_URL, headers={'op': card_details.full_invoice_op}, - data={'secao': 'Cartoes:MinhaFatura', - 'item': ''}) + response = self._session.post( + ROUTER_URL, headers={"op": card_details.full_statement_op}, data=card_id + ) return response.json() def get_statements(self, days=90): diff --git a/pyitau/pages.py b/pyitau/pages.py index 5361115..7781579 100644 --- a/pyitau/pages.py +++ b/pyitau/pages.py @@ -164,6 +164,17 @@ class AuthenticatedHomePage(SoupPage): def op(self): return self._soup.find('div', class_='logo left').find('a').attrs['data-op'] + @property + def menu_op(self): + return re.search( + r"var obterMenu = function\(\) \{" + r'[\n\t\r\s]+var perfil = \$\("#portalTxt"\).val\(\);' + r"[\n\t\r\s]+\$.ajax\(\{" + r'[\n\t\r\s]+url : "([^"]+)"', + self._text, + flags=re.DOTALL, + ).group(1) + class MenuPage(TextPage): @property @@ -174,13 +185,15 @@ def checking_account_op(self): flags=re.DOTALL, ).group(1) + +class Menu2Page(TextPage): @property def checking_cards_op(self): return re.search( - r'urlBox : "([^"]+)"[\n\t\r\s,]*seletorContainer : "#boxCartoes",', + r"'cartoes','homeCategoria'(.*?)\"[\n\r\s\t]+data-op=\'([^\']+)\'", self._text, flags=re.DOTALL, - ).group(1) + ).group(2) class CheckingAccountMenu(TextPage): @@ -239,10 +252,26 @@ def filter_statements_by_month_op(self): class CardDetails(TextPage): @property - def full_invoice_op(self): + def invoice_op(self): + try: + return re.search( + r'if \(habilitaFaturaCotacaoDolar === "true"\) ' + r'{[\n\t\r\s]+urlContingencia = "([^"]+)"', + self._text, + flags=re.DOTALL, + ).group(1) + except AttributeError: + return re.search( + r'if \(habilitaDashboardCotacaoDolar === "true"\) ' + r'{[\n\t\r\s]+urlContingencia = "([^"]+)"', + self._text, + flags=re.DOTALL, + ).group(1) + + @property + def full_statement_op(self): return re.search( - r'if \(habilitaFaturaCotacaoDolar === "true"\) ' - r'{[\n\t\r\s]+urlContingencia = "([^"]+)"', + r"data: cartaoSelecionado.id," r'[\n\t\r\s]+url: "([^"]+)"', self._text, flags=re.DOTALL, ).group(1) diff --git a/tests/conftest.py b/tests/conftest.py index f21f775..ba37ef6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,22 +55,51 @@ def response_card_details(): @pytest.fixture def response_authenticated_home(): return """ - + + + + + + """ @@ -113,6 +142,22 @@ def response_checking_statements(): """ +@pytest.fixture +def response_menu2(): + return """ +
  • + + cartões + +
  • + """ + + @pytest.fixture def response_checking_full_statement(): with open('./tests/responses/checking_account_full_statement.html') as file: diff --git a/tests/pages/test_authenticated_home.py b/tests/pages/test_authenticated_home.py index f5909ee..2ebf877 100644 --- a/tests/pages/test_authenticated_home.py +++ b/tests/pages/test_authenticated_home.py @@ -5,25 +5,8 @@ @pytest.fixture -def response(): - return """ - - """ +def response(response_authenticated_home): + return response_authenticated_home @pytest.fixture @@ -36,5 +19,9 @@ def test_init(response): assert page._soup == BeautifulSoup(response, features='html.parser') +def test_menu_op(page): + assert page.menu_op == 'PYITAU_MENU_OP' + + def test_op(page): assert page.op == 'PYITAU_OP' diff --git a/tests/pages/test_card_details.py b/tests/pages/test_card_details.py index 50a9762..10e9ae1 100644 --- a/tests/pages/test_card_details.py +++ b/tests/pages/test_card_details.py @@ -1,11 +1,21 @@ +import pytest + from pyitau.pages import CardDetails -def test_init(response_card_details: str): +@pytest.fixture +def page(response_card_details): + return CardDetails(response_card_details) + + +def test_init(response_card_details): card_details = CardDetails(response_card_details) assert card_details._text == response_card_details -def test_op(response_card_details: str): - card_details = CardDetails(response_card_details) - assert card_details.full_invoice_op == 'PYITAU_URL_CONTIGENCIA_DOLAR_OP' +def test_invoice_op(page): + assert page.invoice_op == "PYITAU_invoice_op" + + +def test_full_statement_op(page): + assert page.full_statement_op == "PYITAU_full_statement_op" diff --git a/tests/pages/test_menu.py b/tests/pages/test_menu.py index fc0f9cf..9b89a33 100644 --- a/tests/pages/test_menu.py +++ b/tests/pages/test_menu.py @@ -15,7 +15,3 @@ def test_init(response_menu): def test_op(page): assert page.checking_account_op == 'PYITAU_OP_ContaCorrente' - - -def test_checking_cards_op(page: MenuPage): - assert page.checking_cards_op == 'PYITAU_OP_Cartoes' diff --git a/tests/pages/test_menu2.py b/tests/pages/test_menu2.py new file mode 100644 index 0000000..a91bb6c --- /dev/null +++ b/tests/pages/test_menu2.py @@ -0,0 +1,22 @@ +import pytest + +from pyitau.pages import Menu2Page + + +@pytest.fixture +def response(response_menu2): + return response_menu2 + + +@pytest.fixture +def page(response): + return Menu2Page(response) + + +def test_init(response): + page = Menu2Page(response) + assert page._text == response + + +def test_checking_cards_op(page): + assert page.checking_cards_op == 'PYITAU_OP_cartoes' diff --git a/tests/responses/card_details.html b/tests/responses/card_details.html index f2a6d81..c769035 100644 --- a/tests/responses/card_details.html +++ b/tests/responses/card_details.html @@ -1,21 +1,454 @@ - \ No newline at end of file diff --git a/tests/test_get_credit_card_invoice.py b/tests/test_get_credit_card_invoice.py index 571ae39..bb1a326 100644 --- a/tests/test_get_credit_card_invoice.py +++ b/tests/test_get_credit_card_invoice.py @@ -4,7 +4,7 @@ import responses from pyitau.main import ROUTER_URL -from pyitau.pages import AuthenticatedHomePage +from pyitau.pages import AuthenticatedHomePage, CardDetails, Menu2Page @pytest.fixture @@ -38,22 +38,35 @@ def response_checking_card_menu(): """ -@pytest.fixture() +@pytest.fixture def authenticated_home_page(response_authenticated_home): return AuthenticatedHomePage(response_authenticated_home) +@pytest.fixture +def menu2_page(response_menu2): + return Menu2Page(response_menu2) + + +@pytest.fixture +def card_details_page(response_card_details): + return CardDetails(response_card_details) + + @responses.activate def test_get_credit_card_invoice( itau, mocker, authenticated_home_page, - response_menu, - response_checking_card_menu, - response_cards_page, + menu2_page, + card_details_page, response_card_details, + response_menu, + response_menu2, ): itau._home = authenticated_home_page + itau._flow_id = "PYITAU_FLOW_ID" + itau._client_id = "PYITAU_CLIENT_ID" responses.add( responses.POST, @@ -69,27 +82,32 @@ def test_get_credit_card_invoice( responses.add( responses.POST, ROUTER_URL, - body=response_checking_card_menu, - match=[responses.matchers.header_matcher({"op": "PYITAU_OP_Cartoes"})], + body=response_menu2, + match=[responses.matchers.header_matcher({"op": authenticated_home_page.menu_op})], ) responses.add( responses.POST, ROUTER_URL, - body=response_cards_page, + body=response_card_details, match=[ - responses.matchers.header_matcher({"op": "PYITAU_CONTEUDO_BOX_CARTOES_OP"}) + responses.matchers.header_matcher({ + "op": menu2_page.checking_cards_op, + "X-FLOW-ID": itau._flow_id, + "X-CLIENT-ID": itau._client_id, + "X-Requested-With": "XMLHttpRequest", + }) ], ) responses.add( responses.POST, ROUTER_URL, - body=response_card_details, + json={"object": {"data": [{"id": "PYITAU_CARD_ID"}]}}, match=[ - responses.matchers.header_matcher({"op": "PYITAU_FATURA_REDESENHO_OP"}), + responses.matchers.header_matcher({"op": card_details_page.invoice_op}), responses.matchers.urlencoded_params_matcher( - {"idCartao": "PYITAU_CARD_ID"} + {"secao": "Cartoes", "item": "Home"} ), ], ) @@ -97,16 +115,21 @@ def test_get_credit_card_invoice( responses.add( responses.POST, ROUTER_URL, - body="""{"success": true}""", + body='', match=[ - responses.matchers.header_matcher( - {"op": "PYITAU_URL_CONTIGENCIA_DOLAR_OP"} - ), + responses.matchers.header_matcher({"op": card_details_page.invoice_op}), responses.matchers.urlencoded_params_matcher( - {"secao": "Cartoes:MinhaFatura"} + {"secao": "Cartoes:MinhaFatura", "item": ""} ), - ], + ] ) + + responses.add( + responses.POST, + ROUTER_URL, + json={"success": True}, + ) + post_spy = mocker.spy(itau._session, "post") assert itau.get_credit_card_invoice() == {"success": True} @@ -114,17 +137,27 @@ def test_get_credit_card_invoice( call( ROUTER_URL, headers={"op": authenticated_home_page.op, "segmento": "VAREJO"} ), - call(ROUTER_URL, headers={"op": "PYITAU_OP_Cartoes"}), - call(ROUTER_URL, headers={"op": "PYITAU_CONTEUDO_BOX_CARTOES_OP"}), + call(ROUTER_URL, headers={"op": authenticated_home_page.menu_op}), + call(ROUTER_URL, headers={ + "op": menu2_page.checking_cards_op, + "X-FLOW-ID": itau._flow_id, + "X-CLIENT-ID": itau._client_id, + "X-Requested-With": "XMLHttpRequest", + }), call( ROUTER_URL, - headers={"op": "PYITAU_FATURA_REDESENHO_OP"}, - data={"idCartao": "PYITAU_CARD_ID"}, + headers={"op": card_details_page.invoice_op}, + data={"secao": "Cartoes", "item": "Home"}, ), call( ROUTER_URL, - headers={"op": "PYITAU_URL_CONTIGENCIA_DOLAR_OP"}, + headers={"op": card_details_page.invoice_op}, data={"secao": "Cartoes:MinhaFatura", "item": ""}, ), + call( + ROUTER_URL, + headers={"op": card_details_page.full_statement_op}, + data='PYITAU_CARD_ID', + ), ] post_spy.assert_has_calls(calls)