From 9e084b4a1c24ae54ba87d99ab53998db908048c5 Mon Sep 17 00:00:00 2001 From: dafaath Date: Thu, 14 Nov 2024 13:42:48 +0700 Subject: [PATCH] feat(sdk): Adding public key verification on check status --- README.md | 30 +++---- py_sat/client.py | 25 ++++-- py_sat/signature/__init__.py | 4 +- tests/conftest.py | 151 +++++++++++++++++++++++++---------- tests/test_callback.py | 24 ++++-- tests/test_check_status.py | 129 ++++++++++++++++-------------- tests/test_checkout.py | 6 -- tests/test_client.py | 14 ++-- tests/test_signature.py | 17 ++-- 9 files changed, 248 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index f75ad6c..13e240e 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,10 @@ config = ( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE_KEY", # required + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key ) # Below is optional parameter .with_timeout(10) - # Public key is optional, used only for callback signature verification - .with_public_key("SAT_PUBLIC_KEY") .with_is_debug(True) ) @@ -75,11 +74,10 @@ config = ( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE_KEY", # required + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key ) # Below is optional parameter .with_timeout(10) - # Public key is optional, used only for callback signature verification - .with_public_key("SAT_PUBLIC_KEY") # Override SAT Base URL .with_sat_base_url("https://b2b.tokopedia.com/api") ) @@ -113,11 +111,10 @@ config = ( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE_KEY", # required + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key ) # Below is optional parameter .with_timeout(10) - # Public key is optional, used only for callback signature verification - .with_public_key("SAT_PUBLIC_KEY") # Override SAT Base URL .with_sat_base_url("https://b2b.tokopedia.com/api") ) @@ -155,11 +152,10 @@ config = ( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE_KEY", # required + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key ) # Below is optional parameter .with_timeout(10) - # Public key is optional, used only for callback signature verification - .with_public_key("SAT_PUBLIC_KEY") # Override SAT Base URL .with_sat_base_url("https://b2b.tokopedia.com/api") ) @@ -201,11 +197,10 @@ config = ( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE_KEY", # required + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key ) # Below is optional parameter .with_timeout(10) - # Public key is optional, used only for callback signature verification - .with_public_key("SAT_PUBLIC_KEY") # Override SAT Base URL .with_sat_base_url("https://b2b.tokopedia.com/api") ) @@ -254,11 +249,10 @@ config = ( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE_KEY", # required + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key ) # Below is optional parameter .with_timeout(10) - # Public key is optional, used only for callback signature verification - .with_public_key("SAT_PUBLIC_KEY") # Override SAT Base URL .with_sat_base_url("https://b2b.tokopedia.com/api") ) @@ -275,11 +269,10 @@ config = ( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE_KEY", # required + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key ) # Below is optional parameter .with_timeout(10) - # Public key is optional, used only for callback signature verification - .with_public_key("SAT_PUBLIC_KEY") # Override SAT Base URL .with_sat_base_url("https://b2b.tokopedia.com/api") ) @@ -303,7 +296,8 @@ config = SATClientConfig( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE_KEY", # required -).with_public_key("SAT_PUBLIC_KEY") + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key +) sat_client = SATClient(config) @@ -354,7 +348,8 @@ config = SATClientConfig( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE", # required -).with_public_key("SAT_PUBLIC_KEY") + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key +) sat_client = SATClient(config) @@ -387,7 +382,8 @@ config = SATClientConfig( client_id="YOUR_CLIENT_ID", # required client_secret="YOUR_CLIENT_SECRET", # required private_key="YOUR_PRIVATE", # required -).with_public_key("SAT_PUBLIC_KEY") + sat_public_key="SAT_PUBLIC_KEY", # required !IMPORTANT: This is SAT public key, NOT your public key +) try: sat_client = SATClient(config) diff --git a/py_sat/client.py b/py_sat/client.py index 2f5126e..d6a8dee 100644 --- a/py_sat/client.py +++ b/py_sat/client.py @@ -34,9 +34,9 @@ class SATClientConfig: client_id: str client_secret: str private_key: str + sat_public_key: str # Optional - public_key: Optional[str] padding_type: SignatureType is_debug: bool sat_base_url: str @@ -49,6 +49,7 @@ def __init__( client_id: str, client_secret: str, private_key: str, + sat_public_key: str, ): if not client_id or not isinstance(client_id, str): raise InvalidInputException("Client ID are required and must be a string") @@ -61,9 +62,13 @@ def __init__( if not private_key or not isinstance(private_key, str): raise InvalidInputException("Private key is required and must be a string") + if not sat_public_key or not isinstance(sat_public_key, str): + raise InvalidInputException("Public key is required and must be a string") + self.client_id = client_id self.client_secret = client_secret self.private_key = private_key + self.sat_public_key = sat_public_key self._set_default_value() @@ -73,7 +78,6 @@ def _set_default_value(self): logger.setLevel(logging.DEBUG) self.logger = logger - self.public_key = None self.padding_type = SignatureType.PSS self.is_debug = False self.sat_base_url = PLAYGROUND_SAT_BASE_URL @@ -84,10 +88,6 @@ def with_logger(self, logger: logging.Logger): self.logger = logger return self - def with_public_key(self, public_key: str): - self.public_key = public_key - return self - def with_padding_type(self, padding_type: SignatureType): self.padding_type = padding_type return self @@ -122,7 +122,7 @@ class SATClient: def __init__(self, config: SATClientConfig): self._config = config self.signature = Signature( - config.private_key, config.public_key, config.padding_type + config.private_key, config.sat_public_key, config.padding_type ) self._logger = config.logger self._http_client = HTTPClient( @@ -235,7 +235,18 @@ def check_status(self, request_id: str) -> Union[OrderDetail, ErrorResponse]: response = self._http_client.send_request(http_req) response.raise_for_status() + signature = response.headers.get("signature") + if not signature: + raise UnauthenticatedException( + "Signature is not present in the header, please check the request" + ) + + valid = self.signature.verify(response.text, signature) + if not valid: + raise UnauthenticatedException("Signature is not valid") + json_response = response.json() + data = parse_json_api_response(json_response) return OrderDetail.from_dict(data).with_raw_response(response) diff --git a/py_sat/signature/__init__.py b/py_sat/signature/__init__.py index 1838ba6..a897824 100644 --- a/py_sat/signature/__init__.py +++ b/py_sat/signature/__init__.py @@ -24,14 +24,14 @@ class Signature: def __init__( self, private_key_str: Optional[str], - public_key_str: Optional[str], + sat_public_key_str: Optional[str], padding_type: SignatureType, ): if not padding_type: raise InvalidInputException("Padding type is required") self._private_key = self._parse_rsa_private_key_from_pem_str(private_key_str) - self._public_key = self._parse_public_key(public_key_str) + self._public_key = self._parse_public_key(sat_public_key_str) self._algorithm = self.__decide_padding_algorithm(padding_type) def verify(self, msg: str, signature: str) -> bool: diff --git a/tests/conftest.py b/tests/conftest.py index f0c1f2c..e2f66bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,45 +9,88 @@ from py_sat import SATClient, SATClientConfig from py_sat.constant import ACCESS_TOKEN_URL, PLAYGROUND_SAT_BASE_URL +from py_sat.signature import Signature, SignatureType + +TESTING_CLIENT_PRIVATE_KEY = """-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmoG7sJRcmHmDK +wQoPn94VLAvxZtxV2cqG/xEGW+LdREqMzbGHhTrKWMp3MODC2gyoyJ9GVMNG8qxX +R8j+d2BqlI3vIP79fyLG56eGudqRkphhD4RNZiybuK8W0QZGmt86MMrZvubjUgWu +lyX7gFWyjGwAZIFVZSrJe0YdiafgH9VKLrDOTRyqgbJ6Eo3v9VP/Px2AwOCH7HIe +R3XI5ubPcxuzMeKREAlFdrF/b/18vShBaCaAZinQdS7DdcyNu5RwEe2kR8Yga/0X +pklsnYUefes2Yf7W+RjgseSbPINkLUFLAwiS3VGO+bzKCMqGlL1WXuyw2d66KEcM +rTN78TY1AgMBAAECggEAMldWK9Io5ENZSuh3ebD7D7p3AT/qYaWjIpX9NsacC+2N ++GxMrnz5/hhFUy1ZOoVWPcgfFsiVFuJKXzQ47WhzoL+xAgYeA8hdYWqrmnCcME7x +6qEdf6TW5VUu0N3l33764kHLh229pAAr50uTFiD3wzHZj2TODla6TpUH4fSs23FF +2phan8enc4mFHKXUng+e36pjFkdhaVI5kzmtOIzUYukT9KuiEVc6H3eG/aTOukXj +6203BODN/Zfs2gj75cxi9ta3N3UuzcRXrZls2W5exfQERBYpuIkknjejy5EYw7QZ +r0w9nXaRJ3HEcey1J48LeVIquFWeYjNcwQykIt+VIQKBgQDPrCcmzv8DyQQTiZHE +0S9PcISiwZWzwjlyAsk2yBLs/8KLs8sQcrkoJ5vn7AZgUudqBjv6yvjiJqnQlQL1 +2C6D6N4wEl66DOYVb9fDaxsr0kUrpIF52CPEeTfnw9Q4+yo/p11yTbPz691sae4k +IZwwg1XtSotF+mQ9fths2gmaXwKBgQDNZwaKaMdVCdFKdRFe18NDw+CcdS5ipwkf +sBYU+uff9MCBi8Rx9rUQMjW54/BVFOGpUgRRH/duXlB0zuc6pHsOn+a1Ai7jSPU7 +uPZq3oG5vv9qwJkTIYzb6VXCdoeTRPQlR7hzs3jSO73uepGCCKc2JEL42da7m9n6 +mjmpKPEf6wKBgQCFWUOildQGODNX4EQrny7D0bo5UBiyXorIfKV7eak9aVUgo4hG +vYPLFvPzTgkiHNnfqLUm6uI5RR5Rgv1toyzrIsJZF9KfoNy08yYWo1XFI7Wqum0x +Mep1pGiTd5l0JUMRsIQ+e0qL2+5ISRTTOomyVQL95Znci1WGb0bFTpRP/QKBgBrB +leeHuJeKPNofH9Ej+AqmxGZ9GTq+mYCoNmgrOvNAdacqZr+VrIZclAUP/SmIG9Er +nuZWbKvS21Yr8ZEBBgqkp6/ihesTgOZztJ29OFbS24Czb/0+/JNU9NftCsITVF5a +1ls0AMQaBia/jp7Ks8VoudSiw8cSiTWMy4AOlkJbAoGADIta0IWTIHmpbAYq0M8w +MnaKTiinUhhZe+Q5PVZeZjdzt+nt71xk5DmJkSZa8v6FFu18ddRf8CzylFUCFRPp +wC6by4FSjc2APGAKVvZA070W/pxUqG6RjAeZyiLZxOCg5CvGzW+7ZnWFziuBFP20 +CJC5sJrsitvtv5BG64biA88= +-----END PRIVATE KEY----- + +""" + +TESTING_CLIENT_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApqBu7CUXJh5gysEKD5/e +FSwL8WbcVdnKhv8RBlvi3URKjM2xh4U6yljKdzDgwtoMqMifRlTDRvKsV0fI/ndg +apSN7yD+/X8ixuenhrnakZKYYQ+ETWYsm7ivFtEGRprfOjDK2b7m41IFrpcl+4BV +soxsAGSBVWUqyXtGHYmn4B/VSi6wzk0cqoGyehKN7/VT/z8dgMDgh+xyHkd1yObm +z3MbszHikRAJRXaxf2/9fL0oQWgmgGYp0HUuw3XMjbuUcBHtpEfGIGv9F6ZJbJ2F +Hn3rNmH+1vkY4LHkmzyDZC1BSwMIkt1Rjvm8ygjKhpS9Vl7ssNneuihHDK0ze/E2 +NQIDAQAB +-----END PUBLIC KEY----- +""" -TESTING_PRIVATE_KEY = """-----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDyR0kXD0bu1nl8 -nZP+GLI8bSVFbk5yKTu99LlLevTTFLx6sIXfabgKPHIpwr0xGf99yobD1ZNZ276x -ffnMAeILNA5XsvaMnPpVB4kNoqlDaQdd4ICelKQwt90QD9CIGptNLL2wtUgEn1g9 -BV6k5xcT8L9Dw/ZqpMCnfBeGRsWJd84LBfN4lLe5k9MXxE4MLUfTC1xxLmO9C9xw -d92aucyRPkv8n+B9dOlYS8C29huTbregl2rEF32dMyYG1qmVH7ufjM4CX9KdNKA7 -hwnJExqrPvhAtj92Ar0Z5JnPfm8SUjQQhFeySeqSHS+kVEsd/AhrsqMsdUSt9ou2 -xJTJmiL9AgMBAAECggEANHOO5Q1jZassn3gs9T6K/c6CWmr0VD5NhwUfhXIP5U/Q -sz4aqYDFfX/TFmvo0iPG/oh1TxninfpXKS11Aj/pHFRPg5iEzHHitzxbpUZRHz0y -gVYsekiDWGHB2+uUkZazBwz33zUL66ZEr+dE823tPt2oxsa6xyE2bTwOCr2xH96S -eE6280gJ1LtgGKD4TFsmXSkgJIDyF2rT6ZkPeUR36N0zWiSRQbskLaTqwJSpT6mY -hdZRQBvH/6X2JJgOmWWVizE1ChdOVB7rN3tb7NBSbwf5TWNGsNdhQQoTPxAtGiX8 -O+jXilCIjLSqudIDv20jCa8spFuXwpIeBhR/7394GQKBgQD6+g4yZSybGMnMgNM6 -OrL1GnJbxtnuMENMi7e2NgeLxKmpWE3cPqTNe9uWNxXYexDgDLNtAfjw7+UZxO6f -e3cPXuyVsr2xTkEscLBNDXrwQV3phnfwpxX++8Flv0xP34TocWAIKXPcQj8h2SYr -6/zhY40YnRsUgsR0x4eAlqJHBQKBgQD3IKl15jjzHEyqVvWuAjsw3LO8rA96i7vg -WDV5LBrsJgE11M7iurwZxZ71STPVzWK5UzxsZBCJgK41NIvgVx6QIuU0HXw9taXY -7PrAwNcpx7yKksXPYsTGhKumLvFQBY/R9qP2RcFN5WQjTSBxp0B5QMDHNz01dPi6 -l0TfjKO9mQKBgQCZRRJcdmsaQLYkfNwCaIyXoNIL+FFo4/KFkaHc1fwfwDd4ouPR -yDPvBV/hybw+m1F/8mG1BYpY4bhA14J+xPC941OKTEEKQecNU7hnJf9ZMCJBFgyz -W+bT9D10fLIG6VMKfQqPkXkfHxnc+vcTxaeGobwuNuutx/pf8uZugg+SXQKBgQC3 -qjOnpxHeRMMJuhVfXNMm7nA6odnjJuTbyFL9mnTr2xb9LgsQYN4ZfVE1VVFL7hgY -Si9XE0tjFhri+gmXEshpMTYNdHh42H7I6N830FpY99Q9XPXcurgqHkIAAVVhNrD7 -yAV1q8QNo5W30sNxFG+Lbj+YD4rTJvsQmgoa5shuyQKBgQCDJuBgjNgyHqWrKEEt -76OJDiXI4mXDg3N6oCjsP/ZsP7mhkmUsokDS1paSnQxtt0tbft+fpbQSFtDGkRKr -OM5SBQhJEa//gFAofgYt+Fo6wcj1NSgBeLspsYx/avRdj9drVGyxOKJX0zsWQemU -Gf6bGumDG6hy0wu4aWDjSvTmww== +TESTING_SAT_PRIVATE_KEY = """-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCutVhWwBYr32Y5 +ZQw4k6E/dctBKN4F3FETS9QsRbbVKJlX/UHnm4O9K+D9WO4cOUSyaznUfCNL38zY +u90n7kAr/wFA9a75lvnn1m2D95K5EJzh4RtjIwhCozB7kl2gdjUTgZpn1u0IlG0e +Ofwnb/UBiJJ0E+uvAcHrfz5pgsvhHoD1PLeB5o73bpB71BSI4Uay1NoSeZob1vC9 +s4AjmvmZEXtQ8m91aZhvySkjupkbshGheoMwJz6stwdCecVk7G/EeeI8zSH7e5GA +p99I/FgfDY1tzX2l8whziqzmmocO+x7sUXeWB8FzLbhIust7Evt+i5H1sLub/9Jh +5+gN+7UtAgMBAAECggEABHN6ITFHcodNPbtNbbTHkzjUKVntS8Ptst3bNX3yZwlG +Qt+6cDhEY0TwOGdg7AopFGY3KBE5/i+UMsj/UCQGflIPltD+y/AYEagjMsBtFgoo +A3jU1eVC5OoiDMi3wIqFo3R8jRaEna1xmQ2lvXfhcgN02I0+k3JdhCHCFgKvLavE +KoEMPIOYWynXlMSWvAwsNNW/SxrC3NCitHl9Rp3lOcVP/kOWlgIZTrSJeFBGFCni +9zyeu0/Pzwys5VKGPQkED1Is6cEFxr89ZRJ4SzSVMogYxLzZf0KP5yxmGadXIb4X +CMOqfdUdFhRpzgsTOn/+rzXfG1cbtOTP40cxY/nclwKBgQDmljw4C//0AvOhyaA6 +CqfhlqOcBKhGZUg2UOFziFhXCdXjYPlOf0NDW8e5tE7OGWBxBGGty/hhjLmNMDpF +mVVzI6dbY8/64eBrp+IBP87O3xxh/07i2TY7M8fBAnsK1aSUC/FFNcWcspK55IOS +CkgpSwUkn5WYDoClkjGm4OoKGwKBgQDB9o6+LLdDrPymgtS7l+OWmdiSkma6XXp0 +xRKqknbNuXtxHsEVEHouYRqZXFtIETYtrkF72Nab7GpX1cZxZ5XmJU01fbI0baOH +MDuL3/vvtkLAcGCrzTQnB0Mpw23CAMJgbcyKRnA2sSKrd4GubaVGcsrGYiOv3ysB +MW0kAPsyVwKBgB1yF/SMS74sVlJVvhlLXQ7ovrHgwmBi9KrC/1dSlP1gayjjLFMC +22MRqFqllN6qzO8BwTuBbZF/d/54pyhWIVxXtDpub5O5HoCA6tKABHfUc/prsPY1 +CMDcpuiV2YKTr7WcJM5SxI5zG1uTu919ZKOpSdnYazEEwRbjqWWHGTv7AoGAFz9l +Bng3kv313kNKGh3vYkqYQaEYfPfdSIeiYB1j7e5wVDOactrhuhNba8w9CJs/giQj +pyNrPY8Ng++UdF01AzuvUFz7cfs+IWLvkClNegK/Z29QtubGfHMLYsMQsbMDmSkv +3dbpdjSu8hxFx9FOgO4bTcHPgzHdZqw0557SfMsCgYB7oKwXz899tx5+c+D4LXgV +cSdlE/anH8RG6fc0HN8CYuWLnZRtiVKEePhomkql0j16xiQXpypFa01OykXaDQT3 +gkQ02Ye6HHcsfaZdw3Nzp30OyrR9Do9/LHfviesR7larEVvSpMPscbmIJc20lACE +m44DCWLirq3PJ1tHkSvhdQ== -----END PRIVATE KEY----- """ -TESTING_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8kdJFw9G7tZ5fJ2T/hiy -PG0lRW5Ocik7vfS5S3r00xS8erCF32m4CjxyKcK9MRn/fcqGw9WTWdu+sX35zAHi -CzQOV7L2jJz6VQeJDaKpQ2kHXeCAnpSkMLfdEA/QiBqbTSy9sLVIBJ9YPQVepOcX -E/C/Q8P2aqTAp3wXhkbFiXfOCwXzeJS3uZPTF8RODC1H0wtccS5jvQvccHfdmrnM -kT5L/J/gfXTpWEvAtvYbk263oJdqxBd9nTMmBtaplR+7n4zOAl/SnTSgO4cJyRMa -qz74QLY/dgK9GeSZz35vElI0EIRXsknqkh0vpFRLHfwIa7KjLHVErfaLtsSUyZoi -/QIDAQAB +TESTING_SAT_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArrVYVsAWK99mOWUMOJOh +P3XLQSjeBdxRE0vULEW21SiZV/1B55uDvSvg/VjuHDlEsms51HwjS9/M2LvdJ+5A +K/8BQPWu+Zb559Ztg/eSuRCc4eEbYyMIQqMwe5JdoHY1E4GaZ9btCJRtHjn8J2/1 +AYiSdBPrrwHB638+aYLL4R6A9Ty3geaO926Qe9QUiOFGstTaEnmaG9bwvbOAI5r5 +mRF7UPJvdWmYb8kpI7qZG7IRoXqDMCc+rLcHQnnFZOxvxHniPM0h+3uRgKffSPxY +Hw2Nbc19pfMIc4qs5pqHDvse7FF3lgfBcy24SLrLexL7fouR9bC7m//SYefoDfu1 +LQIDAQAB -----END PUBLIC KEY----- """ @@ -64,7 +107,8 @@ def env(): return TestEnvironment[env.upper()] -def __construct_local_test_config(make_httpserver: HTTPServer): +@pytest.fixture(scope="session") +def local_config(make_httpserver: HTTPServer): base_url = make_httpserver.url_for("") base_url_without_trailing_slash = base_url[:-1] @@ -74,10 +118,10 @@ def __construct_local_test_config(make_httpserver: HTTPServer): SATClientConfig( client_id="client_id", client_secret="client_secret", - private_key=TESTING_PRIVATE_KEY, + private_key=TESTING_CLIENT_PRIVATE_KEY, + sat_public_key=TESTING_SAT_PUBLIC_KEY, ) .with_timeout(15) - .with_public_key(TESTING_PUBLIC_KEY) .with_sat_base_url(base_url_without_trailing_slash) .with_access_token_base_url(access_token_base_url) .with_is_debug(True) @@ -94,7 +138,8 @@ def __construct_local_test_config(make_httpserver: HTTPServer): return config -def __construct_sandbox_test_config(): +@pytest.fixture(scope="session") +def sandbox_config(): client_id = os.getenv("PY_SAT_TEST_CLIENT_ID") client_secret = os.getenv("PY_SAT_TEST_CLIENT_SECRET") if not client_id or not client_secret: @@ -112,9 +157,9 @@ def __construct_sandbox_test_config(): client_id=client_id, client_secret=client_secret, private_key=private_key, + sat_public_key=public_key, ) .with_timeout(10) - .with_public_key(public_key) .with_is_debug(True) ) @@ -122,13 +167,15 @@ def __construct_sandbox_test_config(): @pytest.fixture(scope="session") -def sat_config(make_httpserver: HTTPServer, env: TestEnvironment): +def sat_config( + local_config: SATClientConfig, sandbox_config: SATClientConfig, env: TestEnvironment +): if env == TestEnvironment.LOCAL: logging.info("Using local test config") - return __construct_local_test_config(make_httpserver) + return local_config elif env == TestEnvironment.SANDBOX: logging.info("Using sandbox test config") - return __construct_sandbox_test_config() + return sandbox_config else: raise ValueError(f"Invalid environment: {env}") @@ -140,6 +187,24 @@ def sat_client(sat_config: SATClientConfig): return sat_client +@pytest.fixture(scope="session") +def sat_signer(): + return Signature( + padding_type=SignatureType.PSS, + private_key_str=TESTING_SAT_PRIVATE_KEY, + sat_public_key_str=TESTING_SAT_PUBLIC_KEY, + ) + + +@pytest.fixture(scope="session") +def client_signer(): + return Signature( + padding_type=SignatureType.PSS, + private_key_str=TESTING_CLIENT_PRIVATE_KEY, + sat_public_key_str=TESTING_CLIENT_PUBLIC_KEY, + ) + + class TestUtil: @staticmethod def generate_random_string(length: int) -> str: diff --git a/tests/test_callback.py b/tests/test_callback.py index bf768ac..b63aa49 100644 --- a/tests/test_callback.py +++ b/tests/test_callback.py @@ -19,14 +19,20 @@ from werkzeug.wrappers import Request, Response from py_sat import SATClient +from py_sat.client import SATClientConfig from py_sat.models import OrderDetail +from py_sat.signature import Signature, SignatureType from py_sat.utils import parse_json_api_response -def test_callback(make_httpserver: HTTPServer, sat_client: SATClient): +def test_callback( + make_httpserver: HTTPServer, local_config: SATClientConfig, sat_signer: Signature +): """ Example of how to handle the callback from SAT using SAT Python SDK """ + + sat_client = SATClient(local_config) make_httpserver.expect_request( "/callback", method="POST", @@ -58,10 +64,11 @@ def test_callback(make_httpserver: HTTPServer, sat_client: SATClient): }, } } - signature = sat_client.signature.sign(json.dumps(body)) + + sig = sat_signer.sign(json.dumps(body)) headers = { "content-type": "application/vnd.api+json", - "signature": signature, + "signature": sig, } response = requests.post( make_httpserver.url_for("/callback"), @@ -77,6 +84,9 @@ def create_handler(sat_client: SATClient): def handler(request: Request) -> Response: try: data = request.json + if data is None: + raise Exception("data is None") + headers = dict(request.headers) def do_action(order_detail: OrderDetail): @@ -110,11 +120,15 @@ def do_action(order_detail: OrderDetail): return handler -def test_callback_signature(make_httpserver: HTTPServer, sat_client: SATClient): +def test_callback_signature( + make_httpserver: HTTPServer, local_config: SATClientConfig, sat_signer: Signature +): """ Example of how to handle the callback from SAT without using SATClient handle_callback function but only using the signature verification """ + + sat_client = SATClient(local_config) make_httpserver.expect_request( "/callback", method="POST", @@ -146,7 +160,7 @@ def test_callback_signature(make_httpserver: HTTPServer, sat_client: SATClient): }, } } - signature = sat_client.signature.sign(json.dumps(body)) + signature = sat_signer.sign(json.dumps(body)) headers = { "content-type": "application/vnd.api+json", "signature": signature, diff --git a/tests/test_check_status.py b/tests/test_check_status.py index be14ea7..fbd9d35 100644 --- a/tests/test_check_status.py +++ b/tests/test_check_status.py @@ -17,6 +17,7 @@ from py_sat.constant import CHECK_STATUS_PATH, CHECKOUT_PATH, SDK_LABEL from py_sat.models import (ErrorObject, ErrorResponse, Field, OrderDetail, OrderRequest) +from py_sat.signature import Signature from py_sat.utils import generate_json_api_request @@ -24,6 +25,7 @@ def test_check_status_success( make_httpserver: HTTPServer, sat_client: SATClient, util: TestUtil, + sat_signer: Signature, ): """ Test check status success @@ -42,8 +44,6 @@ def test_check_status_success( amount=3500, ) body = generate_json_api_request(req.to_dict()) - body_str = json.dumps(body) - signature = sat_client.signature.sign(body_str) make_httpserver.expect_request( CHECKOUT_PATH, @@ -86,8 +86,6 @@ def test_check_status_success( headers={"Content-Type": "application/json"}, ) - assert sat_client.signature.verify(body_str, signature) - response = sat_client.checkout(req) assert response.is_success() @@ -103,6 +101,40 @@ def test_check_status_success( id=random_string, ) + body = { + "data": { + "type": "order", + "id": random_string, + "attributes": { + "admin_fee": 2500, + "client_name": "Tokopedia User Default", + "client_number": random_client_id, + "error_code": "", + "error_detail": "", + "fields": None, + "fulfilled_at": datetime.datetime.now( + tz=datetime.timezone.utc + ).isoformat(), + "fulfillment_result": [ + {"name": "Nomor Referensi", "value": "174298636"}, + {"name": "Nama Pelanggan", "value": "Tokopedia User Default"}, + {"name": "Nomor Pelanggan", "value": "611981111"}, + {"name": "Jumlah Tagihan", "value": "1"}, + {"name": "Periode Bayar", "value": "Maret 2022"}, + {"name": "Total Tagihan", "value": "Rp 1.000"}, + {"name": "Biaya Admin", "value": "Rp 2.500"}, + {"name": "Total Bayar", "value": "Rp 3.500"}, + ], + "partner_fee": 2000, + "product_code": "speedy-indihome", + "sales_price": 3500, + "serial_number": "174298636", + "status": "Success", + "voucher_code": "", + }, + } + } + make_httpserver.expect_request( CHECK_STATUS_PATH.format(request_id=random_string), headers={ @@ -110,42 +142,13 @@ def test_check_status_success( "authorization": "Bearer testingToken", "X-Sat-Sdk-Version": SDK_LABEL, }, - ).respond_with_json( - response_json={ - "data": { - "type": "order", - "id": random_string, - "attributes": { - "admin_fee": 2500, - "client_name": "Tokopedia User Default", - "client_number": random_client_id, - "error_code": "", - "error_detail": "", - "fields": None, - "fulfilled_at": datetime.datetime.now( - tz=datetime.timezone.utc - ).isoformat(), - "fulfillment_result": [ - {"name": "Nomor Referensi", "value": "174298636"}, - {"name": "Nama Pelanggan", "value": "Tokopedia User Default"}, - {"name": "Nomor Pelanggan", "value": "611981111"}, - {"name": "Jumlah Tagihan", "value": "1"}, - {"name": "Periode Bayar", "value": "Maret 2022"}, - {"name": "Total Tagihan", "value": "Rp 1.000"}, - {"name": "Biaya Admin", "value": "Rp 2.500"}, - {"name": "Total Bayar", "value": "Rp 3.500"}, - ], - "partner_fee": 2000, - "product_code": "speedy-indihome", - "sales_price": 3500, - "serial_number": "174298636", - "status": "Success", - "voucher_code": "", - }, - } - }, + ).respond_with_data( + response_data=json.dumps(body), status=200, - headers={"Content-Type": "application/json"}, + headers={ + "Content-Type": "application/json", + "signature": sat_signer.sign(json.dumps(body)), + }, ) times = 1 @@ -174,6 +177,7 @@ def test_check_status_success( def test_check_status_failed( make_httpserver: HTTPServer, sat_client: SATClient, + sat_signer: Signature, util: TestUtil, ): """ @@ -197,7 +201,6 @@ def test_check_status_failed( ) body = generate_json_api_request(req.to_dict()) body_str = json.dumps(body) - signature = sat_client.signature.sign(body_str) make_httpserver.expect_request( CHECKOUT_PATH, @@ -241,8 +244,6 @@ def test_check_status_failed( headers={"Content-Type": "application/json"}, ) - assert sat_client.signature.verify(body_str, signature) - response = sat_client.checkout(req) assert response.is_success() @@ -259,6 +260,24 @@ def test_check_status_failed( id=random_string, ) + body = { + "data": { + "type": "order", + "id": random_string, + "attributes": { + "client_number": "2121212", + "error_code": "S02", + "error_detail": "Product is not available", + "fields": [{"name": "optional", "value": "optional"}], + "fulfilled_at": None, + "partner_fee": 1000, + "product_code": "pln-postpaid", + "sales_price": 12500, + "serial_number": "", + "status": "Failed", + }, + } + } make_httpserver.expect_request( CHECK_STATUS_PATH.format(request_id=random_string), method="GET", @@ -267,27 +286,13 @@ def test_check_status_failed( "authorization": "Bearer testingToken", "X-Sat-Sdk-Version": SDK_LABEL, }, - ).respond_with_json( - response_json={ - "data": { - "type": "order", - "id": random_string, - "attributes": { - "client_number": "2121212", - "error_code": "S02", - "error_detail": "Product is not available", - "fields": [{"name": "optional", "value": "optional"}], - "fulfilled_at": None, - "partner_fee": 1000, - "product_code": "pln-postpaid", - "sales_price": 12500, - "serial_number": "", - "status": "Failed", - }, - } - }, + ).respond_with_data( + response_data=json.dumps(body), status=200, - headers={"Content-Type": "application/json"}, + headers={ + "Content-Type": "application/json", + "signature": sat_signer.sign(json.dumps(body)), + }, ) times = 1 diff --git a/tests/test_checkout.py b/tests/test_checkout.py index 6da5741..ccfe746 100644 --- a/tests/test_checkout.py +++ b/tests/test_checkout.py @@ -87,8 +87,6 @@ def test_checkout( headers={"Content-Type": "application/json"}, ) - assert sat_client.signature.verify(body_str, signature) - response = sat_client.checkout(req) assert response.to_json(indent=4) @@ -160,8 +158,6 @@ def test_checkout_product_not_found( headers={"Content-Type": "application/json"}, ) - assert sat_client.signature.verify(body_str, signature) - response = sat_client.checkout(req) assert not response.is_success() @@ -274,8 +270,6 @@ def test_checkout_duplicate_request_id( headers={"Content-Type": "application/json"}, ) - assert sat_client.signature.verify(body_str, signature) - response = sat_client.checkout(req) assert response.is_success() diff --git a/tests/test_client.py b/tests/test_client.py index 2c3fe97..c66e9bd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,7 +5,7 @@ """ import pytest -from conftest import TESTING_PRIVATE_KEY +from conftest import TESTING_CLIENT_PRIVATE_KEY from py_sat import SATClient, SATClientConfig from py_sat.exceptions import InvalidInputException @@ -31,6 +31,7 @@ def test_sat_client_error_input(): client_id="client_id", client_secret="client_secret", private_key=None, + sat_public_key=None, ) http_client = SATClient(config) @@ -44,8 +45,9 @@ def test_sat_client_error_padding_input(): config = SATClientConfig( client_id="client_id", client_secret="client_secret", - private_key=TESTING_PRIVATE_KEY, - ).with_padding_type("test") + private_key=TESTING_CLIENT_PRIVATE_KEY, + sat_public_key="test", + ) http_client = SATClient(config) @@ -59,6 +61,7 @@ def test_sat_client_invalid_private_key(): client_id="client_id", client_secret="client_secret", private_key="invalid_private_key", + sat_public_key="test", ) _ = SATClient(config) @@ -74,8 +77,9 @@ def test_sat_client_invalid_public_key(): config = SATClientConfig( client_id="client_id", client_secret="client_secret", - private_key=TESTING_PRIVATE_KEY, - ).with_public_key("invalid_public_key") + private_key=TESTING_CLIENT_PRIVATE_KEY, + sat_public_key="invalid_public_key", + ) _ = SATClient(config) assert "Invalid RSA public key" in str(exc_info.value) diff --git a/tests/test_signature.py b/tests/test_signature.py index 1019bef..ffeb38a 100644 --- a/tests/test_signature.py +++ b/tests/test_signature.py @@ -7,8 +7,9 @@ """ import pytest +from conftest import TESTING_CLIENT_PRIVATE_KEY, TESTING_CLIENT_PUBLIC_KEY -from py_sat import SATClient, SATClientConfig +from py_sat.signature import Signature, SignatureType @pytest.mark.parametrize( @@ -28,7 +29,7 @@ ), ], ) -def test_signature(test_input, test_message, verified, sat_client: SATClient): +def test_signature(test_input, test_message, verified): """ Test signature module. @@ -37,8 +38,14 @@ def test_signature(test_input, test_message, verified, sat_client: SATClient): :param verified: :param sat_client: """ - signature = sat_client.signature.sign(test_input) - assert signature + signature = Signature( + private_key_str=TESTING_CLIENT_PRIVATE_KEY, + sat_public_key_str=TESTING_CLIENT_PUBLIC_KEY, + padding_type=SignatureType.PSS, + ) - is_verified = sat_client.signature.verify(test_message, signature) + sig = signature.sign(test_input) + assert sig + + is_verified = signature.verify(test_message, sig) assert verified == is_verified