From b790c8682534b8c479fe56ab1f3447494f179432 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 21 Mar 2023 20:33:36 -0700 Subject: [PATCH] [PAYOP-1116] Refine network token module interface (#330) * Combine module * Remove raw value parameters * Rename tests * Fix doc --- .../larky/modules/NetworkTokenModule.java | 2 +- larky/src/main/resources/vgs/nts.star | 79 +++++++++++ larky/src/main/resources/vgs/nts_helpers.star | 104 --------------- larky/src/main/resources/vgs/vault.star | 20 --- .../vgs_tests/nts/test_default_nts.star | 90 ++++++++++++- .../vgs_tests/nts/test_nts_helpers.star | 126 ------------------ 6 files changed, 164 insertions(+), 257 deletions(-) create mode 100644 larky/src/main/resources/vgs/nts.star delete mode 100644 larky/src/main/resources/vgs/nts_helpers.star delete mode 100644 larky/src/main/resources/vgs/vault.star delete mode 100644 larky/src/test/resources/vgs_tests/nts/test_nts_helpers.star diff --git a/larky/src/main/java/com/verygood/security/larky/modules/NetworkTokenModule.java b/larky/src/main/java/com/verygood/security/larky/modules/NetworkTokenModule.java index 999ce3e28..675f2216f 100644 --- a/larky/src/main/java/com/verygood/security/larky/modules/NetworkTokenModule.java +++ b/larky/src/main/java/com/verygood/security/larky/modules/NetworkTokenModule.java @@ -18,7 +18,7 @@ import net.starlark.java.eval.StarlarkInt; import net.starlark.java.eval.StarlarkThread; -@StarlarkBuiltin(name = "nts", category = "BUILTIN", doc = "Overridable Network Token API in Larky") +@StarlarkBuiltin(name = "native_nts", category = "BUILTIN", doc = "Overridable Network Token API in Larky") public class NetworkTokenModule implements LarkyNetworkToken { public static final NetworkTokenModule INSTANCE = new NetworkTokenModule(); diff --git a/larky/src/main/resources/vgs/nts.star b/larky/src/main/resources/vgs/nts.star new file mode 100644 index 000000000..3521ebe61 --- /dev/null +++ b/larky/src/main/resources/vgs/nts.star @@ -0,0 +1,79 @@ +load("@vgs//native_nts", _nts="native_nts") +load("@stdlib//larky", larky="larky") +load("@vendor//jsonpath_ng", jsonpath_ng="jsonpath_ng") + + +def render( + input, + pan, + cvv, + amount, + currency_code, + exp_month=None, + exp_year=None, + cryptogram_value=None, + cryptogram_eci=None, +): + """Retrieves a network token for the given PAN alias, renders the cryptogram, and injects the network token values + into the payload. + + For the output JSONPaths, please note that inserting a value into a non-existing deep nested note is not currently + supported. For example, for an input payload like this:: + + input = { + "data": {} + } + + To insert into `$.data.network_token.exp_month` JSONPath, you need to place any value at the exact path first + like this in order to make JSONPath value insertion work:: + + input["data"]["network_token"] = {"exp_month": "TO_BE_REPLACED"} + nts.render(input, ...) + + :param input: JSON payload to inject network token into + :param pan: JSONPath to the PAN alias in the input payload. Used to look up the corresponding network token to be + rendered and injected into the payload. + :param cvv: JSONPath to the CVV of the credit card in the input payload. Used to pass to the network for retrieving + the corresponding network token and cryptogram to be returned. + :param amount: JSONPath to the amount of payment for the transaction to be made with the network token in the input + payload. Used to pass to the network for retrieving the corresponding network token and cryptogram to be + returned. + :param currency_code: JSONPath to the currency code of payment amount for the transaction to be made with the + network token in the input payload. Used to pass to the network for retrieving the corresponding network + token and cryptogram to be returned. + :param exp_month: JSONPath to insert the expiration month of the network token within the input payload + :param exp_year: JSONPath to insert the expiration year of the network token within the input payload + :param cryptogram_value: JSONPath to insert the cryptogram value of the network token within the input + payload + :param cryptogram_eci: JSONPath to insert the cryptogram ECI of the network token within the input payload + :return: JSON payload injected with network token values + """ + pan_value = jsonpath_ng.parse(pan).find(input).value + cvv_value = jsonpath_ng.parse(cvv).find(input).value + amount_value = str(jsonpath_ng.parse(amount).find(input).value) + currency_code_value = jsonpath_ng.parse(currency_code).find(input).value + + network_token = _nts.get_network_token( + pan=pan_value, + cvv=cvv_value, + amount=amount_value, + currency_code=currency_code_value, + ) + placements = [ + (pan, network_token["token"]), + (exp_month, network_token["exp_month"]), + (exp_year, network_token["exp_year"]), + (cryptogram_value, network_token["cryptogram_value"]), + (cryptogram_eci, network_token["cryptogram_eci"]), + ] + for path, value in placements: + if path == None: + continue + input = jsonpath_ng.parse(path).update(input, value).value + return input + + +nts = larky.struct( + get_network_token=_nts.get_network_token, + render=render, +) diff --git a/larky/src/main/resources/vgs/nts_helpers.star b/larky/src/main/resources/vgs/nts_helpers.star deleted file mode 100644 index 741e04541..000000000 --- a/larky/src/main/resources/vgs/nts_helpers.star +++ /dev/null @@ -1,104 +0,0 @@ -load("@stdlib//larky", larky="larky") -load("@vendor//jsonpath_ng", jsonpath_ng="jsonpath_ng") -load("@vgs//nts", "nts") - - -def render( - input, - pan, - cvv, - amount, - currency_code, - raw_pan=False, - raw_cvv=False, - raw_amount=False, - raw_currency_code=False, - output_pan=None, - output_exp_month=None, - output_exp_year=None, - output_cryptogram_value=None, - output_cryptogram_eci=None, -): - """Retrieves a network token for the given PAN alias, renders the cryptogram, and injects the network token values - into the payload. - - For the output JSONPaths, please note that inserting a value into a non-existing deep nested note is not currently - supported. For example, for an input payload like this:: - - input = { - "data": {} - } - - To insert into `$.data.network_token.exp_month` JSONPath, you need to place an empty value at the exact path first - like this in order to make JSONPath value insertion work:: - - input["data"]["network_token"] = {"exp_month": "TO_BE_REPLACED"} - nts_helpers.render(input, ...) - - :param input: JSON payload to inject network token into - :param pan: JSONPath to the PAN alias in the input payload or a raw PAN alias value if `raw_pan` is true. - Used to look up the corresponding network token to be rendered and injected into the payload. - :param cvv: JSONPath to the CVV of the credit card in the input payload or a raw CVV value if `raw_cvv` is true. - Used to pass to the network for retrieving the corresponding network token and cryptogram to be returned. - :param amount: JSONPath to the amount of payment for the transaction to be made with the network token in the input - payload or a raw amount value if `raw_amount` is true. Used to pass to the network for retrieving the - corresponding network token and cryptogram to be returned. - :param currency_code: JSONPath to the currency code of payment amount for the transaction to be made with the - network token in the input payload or a raw amount value if `raw_amount` is true. Used to pass to the - network for retrieving the corresponding network token and cryptogram to be returned. - :param raw_pan: treat `pan` value as a raw input value instead of a JSONPath value - :param raw_cvv: treat `raw_cvv` value as a raw input value instead of a JSONPath value - :param raw_amount: treat `raw_amount` value as a raw input value instead of a JSONPath value - :param raw_currency_code: treat `raw_currency_code` value as a raw input value instead of a JSONPath value - :param output_pan: JSONPath to insert the PAN value of the network token within the input payload. - By default, the `pan` JSONPath path value will be used if no not provided. - :param output_exp_month: JSONPath to insert the expiration month of the network token within the input payload - :param output_exp_year: JSONPath to insert the expiration year of the network token within the input payload - :param output_cryptogram_value: JSONPath to insert the cryptogram value of the network token within the input - payload - :param output_cryptogram_eci: JSONPath to insert the cryptogram ECI of the network token within the input payload - :return: JSON payload injected with network token values - """ - if raw_pan: - pan_value = pan - else: - pan_value = jsonpath_ng.parse(pan).find(input).value - if raw_cvv: - cvv_value = cvv - else: - cvv_value = jsonpath_ng.parse(cvv).find(input).value - if raw_amount: - amount_value = amount - else: - amount_value = str(jsonpath_ng.parse(amount).find(input).value) - if raw_currency_code: - currency_code_value = currency_code - else: - currency_code_value = jsonpath_ng.parse(currency_code).find(input).value - - network_token = nts.get_network_token( - pan=pan_value, - cvv=cvv_value, - amount=amount_value, - currency_code=currency_code_value, - ) - output_pan_jp = output_pan - if output_pan_jp == None and not raw_pan: - output_pan_jp = pan - placements = [ - (output_pan_jp, network_token["token"]), - (output_exp_month, network_token["exp_month"]), - (output_exp_year, network_token["exp_year"]), - (output_cryptogram_value, network_token["cryptogram_value"]), - (output_cryptogram_eci, network_token["cryptogram_eci"]), - ] - for path, value in placements: - if path == None: - continue - input = jsonpath_ng.parse(path).update(input, value).value - return input - - -nts_helpers = larky.struct( - render=render -) diff --git a/larky/src/main/resources/vgs/vault.star b/larky/src/main/resources/vgs/vault.star deleted file mode 100644 index ba783ec07..000000000 --- a/larky/src/main/resources/vgs/vault.star +++ /dev/null @@ -1,20 +0,0 @@ -load("@vgs//vault", _vault="vault") -load("@stdlib/larky", "larky") - -def redact(value, storage=None, format=None, tags=[]): - """ - generates an alias for value - """ - return _vault.redact(value, storage, format, tags) - - -def reveal(value, storage=None): - """ - reveals aliased value - """ - return _vault.reveal(value, storage) - - -vault = larky.struct( - redact = redact, - reveal = reveal) \ No newline at end of file diff --git a/larky/src/test/resources/vgs_tests/nts/test_default_nts.star b/larky/src/test/resources/vgs_tests/nts/test_default_nts.star index c4ca28126..253d233ca 100644 --- a/larky/src/test/resources/vgs_tests/nts/test_default_nts.star +++ b/larky/src/test/resources/vgs_tests/nts/test_default_nts.star @@ -3,6 +3,36 @@ load("@stdlib//unittest", "unittest") load("@vgs//nts", "nts") +def _make_fixture(): + return { + "merchantAccount": "YOUR_MERCHANT_ACCOUNT", + "reference": "YOUR_PAYMENT_REFERENCE", + "amount": { + "currency": "USD", + "value": 1000, + }, + "paymentMethod": { + "type": "networkToken", + "holderName": "CARDHOLDER_NAME", + "number": "785840aLpH4nUmV9985", + "expiryMonth": "TO_BE_REPLACED", + "expiryYear": "TO_BE_REPLACED", + "cvv": "123", + }, + "returnUrl": "https://your-company.com/", + "shopperReference": "YOUR_SHOPPER_REFERENCE", + "recurringProcessingModel": "CardOnFile", + "shopperInteraction": "Ecommerce", + # Deep JSONPath is not supported by the JSONPath lib, so that we need to + # create an empty object here manually. + # ref: https://github.com/json-path/JsonPath/issues/83 + "mpiData": { + "cavv": "TO_BE_REPLACED", + "eci": "TO_BE_REPLACED", + } + } + + def _test_get_network_token(): output = nts.get_network_token( pan="MOCK_PAN_ALIAS", @@ -19,12 +49,12 @@ def _test_get_network_token(): }) -def _test_pan_empty_value(): +def _test_get_network_token_pan_empty_value(): asserts.assert_fails(lambda: nts.get_network_token("", cvv="MOCK_CVV", amount="123.45", currency_code="USD"), "pan argument cannot be blank") -def _test_not_found(): +def _test_get_network_token_not_found(): input = { "pan": "NOT_FOUND", } @@ -33,14 +63,62 @@ def _test_not_found(): "network token is not found") +def _test_render(): + output = nts.render( + _make_fixture(), + pan="$.paymentMethod.number", + cvv="$.paymentMethod.cvv", + amount="$.amount.value", + currency_code="$.amount.currency", + exp_month="$.paymentMethod.expiryMonth", + exp_year="$.paymentMethod.expiryYear", + cryptogram_value="$.mpiData.cavv", + cryptogram_eci="$.mpiData.eci", + ) + asserts.assert_that(output["paymentMethod"]["number"]).is_equal_to("4242424242424242") + asserts.assert_that(output["paymentMethod"]["expiryMonth"]).is_equal_to(12) + asserts.assert_that(output["paymentMethod"]["expiryYear"]).is_equal_to(27) + asserts.assert_that(output["mpiData"]["cavv"]).is_equal_to("MOCK_CRYPTOGRAM_VALUE") + asserts.assert_that(output["mpiData"]["eci"]).is_equal_to("MOCK_CRYPTOGRAM_ECI") + + +def _test_render_pan_empty_value(): + asserts.assert_fails( + lambda: nts.render( + {"pan": "", "cvv": "MOCK_CVV", "amount": "MOCK_AMOUNT", "currency_code": "MOCK_CURRENCY_CODE"}, + pan="$.pan", + cvv="$.cvv", + amount="$.amount", + currency_code="$.currency_code", + ), + "pan argument cannot be blank", + ) + + +def _test_render_not_found(): + asserts.assert_fails( + lambda: nts.render( + {"pan": "NOT_FOUND", "cvv": "MOCK_CVV", "amount": "MOCK_AMOUNT", "currency_code": "MOCK_CURRENCY_CODE"}, + pan="$.pan", + cvv="$.cvv", + amount="$.amount", + currency_code="$.currency_code", + ), + "network token is not found", + ) + + def _suite(): _suite = unittest.TestSuite() - # Redact Tests + # Get network token tests _suite.addTest(unittest.FunctionTestCase(_test_get_network_token)) - _suite.addTest(unittest.FunctionTestCase(_test_pan_empty_value)) - _suite.addTest(unittest.FunctionTestCase(_test_not_found)) - + _suite.addTest(unittest.FunctionTestCase(_test_get_network_token_pan_empty_value)) + _suite.addTest(unittest.FunctionTestCase(_test_get_network_token_not_found)) + # Render tests + _suite.addTest(unittest.FunctionTestCase(_test_render)) + _suite.addTest(unittest.FunctionTestCase(_test_render_pan_empty_value)) + _suite.addTest(unittest.FunctionTestCase(_test_render_not_found)) return _suite diff --git a/larky/src/test/resources/vgs_tests/nts/test_nts_helpers.star b/larky/src/test/resources/vgs_tests/nts/test_nts_helpers.star deleted file mode 100644 index a763c9ff6..000000000 --- a/larky/src/test/resources/vgs_tests/nts/test_nts_helpers.star +++ /dev/null @@ -1,126 +0,0 @@ -load("@vendor//asserts", "asserts") -load("@vgs//nts_helpers", "nts_helpers") -load("@stdlib//unittest", "unittest") - - -def make_fixture(): - return { - "merchantAccount": "YOUR_MERCHANT_ACCOUNT", - "reference": "YOUR_PAYMENT_REFERENCE", - "amount": { - "currency": "USD", - "value": 1000, - }, - "paymentMethod": { - "type": "networkToken", - "holderName": "CARDHOLDER_NAME", - "number": "785840aLpH4nUmV9985", - "expiryMonth": "TO_BE_REPLACED", - "expiryYear": "TO_BE_REPLACED", - "cvv": "123", - }, - "returnUrl": "https://your-company.com/", - "shopperReference": "YOUR_SHOPPER_REFERENCE", - "recurringProcessingModel": "CardOnFile", - "shopperInteraction": "Ecommerce", - # Deep JSONPath is not supported by the JSONPath lib, so that we need to - # create an empty object here manually. - # ref: https://github.com/json-path/JsonPath/issues/83 - "mpiData": { - "cavv": "TO_BE_REPLACED", - "eci": "TO_BE_REPLACED", - } - } - - -def _test_render(): - output = nts_helpers.render( - make_fixture(), - pan="$.paymentMethod.number", - cvv="$.paymentMethod.cvv", - amount="$.amount.value", - currency_code="$.amount.currency", - output_exp_month="$.paymentMethod.expiryMonth", - output_exp_year="$.paymentMethod.expiryYear", - output_cryptogram_value="$.mpiData.cavv", - output_cryptogram_eci="$.mpiData.eci", - ) - asserts.assert_that(output["paymentMethod"]["number"]).is_equal_to("4242424242424242") - asserts.assert_that(output["paymentMethod"]["expiryMonth"]).is_equal_to(12) - asserts.assert_that(output["paymentMethod"]["expiryYear"]).is_equal_to(27) - asserts.assert_that(output["mpiData"]["cavv"]).is_equal_to("MOCK_CRYPTOGRAM_VALUE") - asserts.assert_that(output["mpiData"]["eci"]).is_equal_to("MOCK_CRYPTOGRAM_ECI") - - -def _test_render_with_raw_values_input(): - output = nts_helpers.render( - make_fixture(), - pan="MOCK_PAN_ALIAS", - cvv="123", - amount="45.67", - currency_code="USD", - output_pan="$.paymentMethod.number", - output_exp_month="$.paymentMethod.expiryMonth", - output_exp_year="$.paymentMethod.expiryYear", - output_cryptogram_value="$.mpiData.cavv", - output_cryptogram_eci="$.mpiData.eci", - raw_pan=True, - raw_amount=True, - raw_cvv=True, - raw_currency_code=True - ) - asserts.assert_that(output["paymentMethod"]["number"]).is_equal_to("4242424242424242") - asserts.assert_that(output["paymentMethod"]["expiryMonth"]).is_equal_to(12) - asserts.assert_that(output["paymentMethod"]["expiryYear"]).is_equal_to(27) - asserts.assert_that(output["mpiData"]["cavv"]).is_equal_to("MOCK_CRYPTOGRAM_VALUE") - asserts.assert_that(output["mpiData"]["eci"]).is_equal_to("MOCK_CRYPTOGRAM_ECI") - - -def _test_render_pan_empty_value(): - asserts.assert_fails( - lambda: nts_helpers.render( - {}, - pan="", - cvv="MOCK_CVV", - amount="MOCK_AMOUNT", - currency_code="MOCK_CURRENCY_CODE", - raw_pan=True, - raw_amount=True, - raw_cvv=True, - raw_currency_code=True - ), - "pan argument cannot be blank", - ) - - -def _test_render_not_found(): - asserts.assert_fails( - lambda: nts_helpers.render( - {}, - pan="NOT_FOUND", - cvv="MOCK_CVV", - amount="MOCK_AMOUNT", - currency_code="MOCK_CURRENCY_CODE", - raw_pan=True, - raw_amount=True, - raw_cvv=True, - raw_currency_code=True - ), - "network token is not found", - ) - - -def _suite(): - _suite = unittest.TestSuite() - - # Redact Tests - _suite.addTest(unittest.FunctionTestCase(_test_render)) - _suite.addTest(unittest.FunctionTestCase(_test_render_with_raw_values_input)) - _suite.addTest(unittest.FunctionTestCase(_test_render_pan_empty_value)) - _suite.addTest(unittest.FunctionTestCase(_test_render_not_found)) - - return _suite - - -_runner = unittest.TextTestRunner() -_runner.run(_suite())