diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index be546f2acc3a..25c1e8bfad2a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1064,6 +1064,7 @@ in { systemd-userdbd = handleTest ./systemd-userdbd.nix {}; systemd-homed = handleTest ./systemd-homed.nix {}; systemtap = handleTest ./systemtap.nix {}; + taler = handleTest ./taler {}; tandoor-recipes = handleTest ./tandoor-recipes.nix {}; tandoor-recipes-script-name = handleTest ./tandoor-recipes-script-name.nix {}; tang = handleTest ./tang.nix {}; diff --git a/nixos/tests/taler/common/nodes.nix b/nixos/tests/taler/common/nodes.nix new file mode 100644 index 000000000000..3e8b81ed5179 --- /dev/null +++ b/nixos/tests/taler/common/nodes.nix @@ -0,0 +1,200 @@ +{ lib, ... }: +let + # Forward SSH and WebUI ports to host machine + # + # Connect with: ssh root@localhost -p + # Access WebUI from: http://localhost: + # + # NOTE: This is only accessible from an interactive test, for example: + # $ eval $(nix-build -A nixosTests.taler.basic.driver)/bin/nixos-test-driver + mkNode = + { + sshPort ? 0, + webuiPort ? 0, + nodeSettings ? { }, + }: + lib.recursiveUpdate { + services.openssh = { + enable = true; + settings = { + PermitRootLogin = "yes"; + PermitEmptyPasswords = "yes"; + }; + }; + security.pam.services.sshd.allowNullPassword = true; + virtualisation.forwardPorts = + (lib.optionals (sshPort != 0) [ + { + from = "host"; + host.port = sshPort; + guest.port = 22; + } + ]) + ++ (lib.optionals (webuiPort != 0) [ + { + from = "host"; + host.port = webuiPort; + guest.port = webuiPort; + } + ]); + } nodeSettings; +in +rec { + CURRENCY = "KUDOS"; + FIAT_CURRENCY = "CHF"; + + nodes = { + exchange = + { config, lib, ... }: + mkNode { + sshPort = 1111; + webuiPort = 8081; + + nodeSettings = { + services.taler = { + settings = { + taler.CURRENCY = CURRENCY; + }; + includes = [ ../conf/taler-accounts.conf ]; + exchange = { + enable = true; + debug = true; + openFirewall = true; + denominationConfig = lib.readFile ../conf/taler-denominations.conf; + settings = { + exchange = { + MASTER_PUBLIC_KEY = "2TQSTPFZBC2MC4E52NHPA050YXYG02VC3AB50QESM6JX1QJEYVQ0"; + BASE_URL = "http://exchange:8081/"; + }; + exchange-offline = { + MASTER_PRIV_FILE = "${../conf/private.key}"; + }; + }; + }; + }; + }; + }; + + bank = + { config, ... }: + mkNode { + sshPort = 2222; + webuiPort = 8082; + + nodeSettings = { + services.libeufin.bank = { + enable = true; + debug = true; + + openFirewall = true; + createLocalDatabase = true; + + initialAccounts = [ + { + username = "exchange"; + password = "exchange"; + name = "Exchange"; + } + ]; + + settings = { + libeufin-bank = { + WIRE_TYPE = "x-taler-bank"; + # WIRE_TYPE = "iban"; + X_TALER_BANK_PAYTO_HOSTNAME = "bank:8082"; + # IBAN_PAYTO_BIC = "SANDBOXX"; + BASE_URL = "bank:8082"; + + # Allow creating new accounts + ALLOW_REGISTRATION = "yes"; + + # A registration bonus makes withdrawals easier since the + # bank account balance is not empty + REGISTRATION_BONUS_ENABLED = "yes"; + REGISTRATION_BONUS = "${CURRENCY}:100"; + + DEFAULT_DEBT_LIMIT = "${CURRENCY}:500"; + + # NOTE: The exchange's bank account must be initialised before + # the main bank service starts, else it doesn't work. + # The `services.libeufin.bank.initialAccounts` option can be used to do this. + ALLOW_CONVERSION = "yes"; + ALLOW_EDIT_CASHOUT_PAYTO_URI = "yes"; + + SUGGESTED_WITHDRAWAL_EXCHANGE = "http://exchange:8081/"; + + inherit CURRENCY FIAT_CURRENCY; + }; + }; + }; + + services.libeufin.nexus = { + enable = true; + debug = true; + + openFirewall = true; + createLocalDatabase = true; + + settings = { + # https://docs.taler.net/libeufin/setup-ebics-at-postfinance.html + nexus-ebics = { + # == Mandatory == + CURRENCY = FIAT_CURRENCY; + # Bank + HOST_BASE_URL = "https://isotest.postfinance.ch/ebicsweb/ebicsweb"; + BANK_DIALECT = "postfinance"; + # EBICS IDs + HOST_ID = "PFEBICS"; + USER_ID = "PFC00639"; + PARTNER_ID = "PFC00639"; + # Account information + IBAN = "CH4740123RW4167362694"; + BIC = "BIC"; + NAME = "nixosTest nixosTest"; + + # == Optional == + CLIENT_PRIVATE_KEYS_FILE = "${../conf/client-ebics-keys.json}"; + BANK_PUBLIC_KEYS_FILE = "${../conf/bank-ebics-keys.json}"; + }; + }; + }; + }; + }; + + merchant = + { config, ... }: + mkNode { + sshPort = 3333; + webuiPort = 8083; + + nodeSettings = { + services.taler = { + settings = { + taler.CURRENCY = CURRENCY; + }; + merchant = { + enable = true; + debug = true; + openFirewall = true; + settings.merchant-exchange-test = { + EXCHANGE_BASE_URL = "http://exchange:8081/"; + MASTER_KEY = "2TQSTPFZBC2MC4E52NHPA050YXYG02VC3AB50QESM6JX1QJEYVQ0"; + inherit CURRENCY; + }; + }; + }; + }; + }; + + client = + { pkgs, ... }: + mkNode { + sshPort = 4444; + + nodeSettings = { + environment.systemPackages = [ pkgs.taler-wallet-core ]; + }; + }; + }; + +} diff --git a/nixos/tests/taler/common/scripts.nix b/nixos/tests/taler/common/scripts.nix new file mode 100644 index 000000000000..3860ae229070 --- /dev/null +++ b/nixos/tests/taler/common/scripts.nix @@ -0,0 +1,107 @@ +{ + lib, + pkgs, + nodes, + ... +}: + +let + cfgNodes = pkgs.callPackage ./nodes.nix { inherit lib; }; + bankConfig = nodes.bank.config.environment.etc."libeufin/libeufin.conf".source; + + inherit (cfgNodes) CURRENCY FIAT_CURRENCY; +in +{ + commonScripts = + # python + '' + def succeed(machine, commands): + """A more convenient `machine.succeed` that supports multi-line inputs""" + flattened_commands = [c.replace("\n", "") for c in commands] # flatten multi-line + return machine.succeed(" ".join(flattened_commands)) + + + def systemd_run(machine, cmd, user="nobody", group="nobody"): + """Execute command as a systemd DynamicUser""" + machine.log(f"Executing command (via systemd-run): \"{cmd}\"") + + (status, out) = machine.execute( " ".join([ + "systemd-run", + "--service-type=exec", + "--quiet", + "--wait", + "-E PATH=\"$PATH\"", + "-p StandardOutput=journal", + "-p StandardError=journal", + "-p DynamicUser=yes", + f"-p Group={group}" if group != "nobody" else "", + f"-p User={user}" if user != "nobody" else "", + f"$SHELL -c '{cmd}'" + ]) ) + + if status != 0: + raise Exception(f"systemd_run failed (status {status})") + + machine.log("systemd-run finished successfully") + + + def register_bank_account(username, password, name, is_exchange=False): + """Register Libeufin bank account for the x-taler-bank wire method""" + return systemd_run(bank, " ".join([ + 'libeufin-bank', + 'create-account', + '-c ${bankConfig}', + f'--username {username}', + f'--password {password}', + f'--name {name}', + f'--payto_uri="payto://x-taler-bank/bank:8082/{username}?receiver-name={name}"', + '--exchange' if (is_exchange or username.lower()=="exchange") else ' ' + ]), + user="libeufin-bank") + + + def wallet_cli(command): + """Wrapper for the Taler CLI wallet""" + return client.succeed( + "taler-wallet-cli " + "--no-throttle " # don't do any request throttling + + command + ) + + + def verify_balance(balanceWanted: str): + """Compare Taler CLI wallet balance with expected amount""" + balance = wallet_cli("balance --json") + try: + balanceGot = json.loads(balance)["balances"][0]["available"] + except: + balanceGot = "${CURRENCY}:0" + + # Compare balance with expected value + if balanceGot != balanceWanted: + client.fail(f'echo Wanted balance: "{balanceWanted}", got: "{balanceGot}"') + else: + client.succeed(f"echo Withdraw successfully made. New balance: {balanceWanted}") + + + def verify_conversion(regionalWanted: str): + """Compare converted Libeufin Nexus funds with expected regional currency""" + # Get transaction details + response = json.loads( + succeed(bank, [ + "curl -sSfL", + # TODO: get exchange from config? + "-u exchange:exchange", + "http://bank:8082/accounts/exchange/transactions" + ]) + ) + amount = response["transactions"][0]["amount"].split(":") # CURRENCY:VALUE + currencyGot, regionalGot = amount + + # Check conversion (1:1 ratio) + if (regionalGot != regionalWanted) or (currencyGot != "${CURRENCY}"): + client.fail(f'echo Wanted "${CURRENCY}:{regionalWanted}", got: "{currencyGot}:{regionalGot}"') + else: + client.succeed(f'echo Conversion successfully made: "${FIAT_CURRENCY}:{regionalWanted}" -> "{currencyGot}:{regionalGot}"') + ''; +} diff --git a/nixos/tests/taler/conf/bank-ebics-keys.json b/nixos/tests/taler/conf/bank-ebics-keys.json new file mode 100644 index 000000000000..e3ebadb6ba5b --- /dev/null +++ b/nixos/tests/taler/conf/bank-ebics-keys.json @@ -0,0 +1 @@ +{"bank_encryption_public_key":"621028HG1M30JAM6923FE381040GA003G80GY01GG80GM0M2040G1EACATA11EF5SVKNBNBYF1S3WSKQ2A2R9VZ7RW2HRX00293JPZ7VQ780RFRVYTQKKDDNJAQGBH4659GT9QYBMJCG1RKZEH1WDJ0GAAY7B7NBMW6FWXCKFYRMZQME0WBGZ1AAMY2VBQ5XAFV8216EFNF2EPG6M5ZGHG9RG6EGED56TK9JESQ02Q7AAVBRAAARVBN9NHCN64KQ3SRRHYXB8RWRK4TSSC93XG8RWMQH4ZDJSBYDCEXFY6G3AWTZ0EZNCJJAYB98T4GNFWZMN81AVYCQHXT1APX81AXCAYNK7J9XETF5CN1J1WV0BVA2BYG4VAMAW123REPN67JF1TNWPTADBMHS17N2V1GFYT8JRWX4TGM2996NXTEPMA8C2CDDE0CRY2A6HT8C5H2D6C62YGRSCF820C0G008","bank_authentication_public_key":"621028HG1M30JAM6923FE381040GA003G80GY01GG80GM0M2040G1EACATA11EF5SVKNBNBYF1S3WSKQ2A2R9VZ7RW2HRX00293JPZ7VQ780RFRVYTQKKDDNJAQGBH4659GT9QYBMJCG1RKZEH1WDJ0GAAY7B7NBMW6FWXCKFYRMZQME0WBGZ1AAMY2VBQ5XAFV8216EFNF2EPG6M5ZGHG9RG6EGED56TK9JESQ02Q7AAVBRAAARVBN9NHCN64KQ3SRRHYXB8RWRK4TSSC93XG8RWMQH4ZDJSBYDCEXFY6G3AWTZ0EZNCJJAYB98T4GNFWZMN81AVYCQHXT1APX81AXCAYNK7J9XETF5CN1J1WV0BVA2BYG4VAMAW123REPN67JF1TNWPTADBMHS17N2V1GFYT8JRWX4TGM2996NXTEPMA8C2CDDE0CRY2A6HT8C5H2D6C62YGRSCF820C0G008","accepted":true} diff --git a/nixos/tests/taler/conf/client-ebics-keys.json b/nixos/tests/taler/conf/client-ebics-keys.json new file mode 100644 index 000000000000..d599e86893de --- /dev/null +++ b/nixos/tests/taler/conf/client-ebics-keys.json @@ -0,0 +1 @@ +{"signature_private_key":"62109F020403038614N8CJ46YW6G20810M0090G4MRR84152080G00M2040G1G344ZCHJWVZJVC7X42WJ6H99ZM4MND4QA766HQXG24QCE3FMAZWVSDWJYRPNM0PVWB1AH39685XN11P7EEHDHW89W0XNDYB4EMYTMA771DVMQWV57NZA9C5QGAK2N7FGKE73020ENJF74N49DEQCXW78FQQNZSDPAC07Y0GNJCR53Q59Z97T14E29NRKFMFYF2CQK45TPAK8M80H8K275TG5FMW16YVTSK3YJBSJN0G7MT187QCNS6P25SJD9Q30K578H3YY30F6R852ZG904BW07PAZT29HTV5F3ZE5K86F4MAQ9H2H7C3AYG8P6SZEK60E0QWWX79M0SP2BAC0QT8310X0TDZ16298SBSD5SDFGQT6FG44BEQNES1KVW1JZYD8GSB6TQQZ3VDFE7N9SBNST820C0G0082G7ZNG0X14DZMZZNP1NY2FQCKTRWN6JJVJR8F6AF76PWSAJ2A6R4P8MAMAJATDC0HKKQ6M9JDTFXRX9J8TFPZSEWD2N3CYZP39PGJG26GE8F6EN5P367K2JX45W1GC0FJF2Q1Z131QMKMYQNKT87C8Q5WXXKZ010H6N6YD47PFHBM3KPTEJJ0MSVH9RTCCABKA5MJ7N262BPRVYD6Y4G0Q3TDNYKX62MRXB4GAGS1CEEEGMYZ6TSC2WMWTVJYJCG4ZSDM2D9GEJ5KTH2RTWSJCYHN6E807VQM3GMMZCFCQP4309H592WQE93R2Y93VWD6PDQRJEXSZJDM4F7XHB9BNGAP4S1T67971K1AJQ2PSJXC99KFF8D1JN7GH40E8ZC3W599Y0WHKSD69Q1SB0WCEQEGZ90G50C103FCEV1ZD5CXVQ4T2Q438DGWKVE1K2JVR4EAKYT9AQ7F7EQZ35J04ACC2XVXZ8DN2ZDRY70WDK36GDRGFKZ2SFPACAQ7QAYXJW4PT3VGXAXP78HD8X9T608FY92SJWEE235AAYR26FEYX9VKQ6RH7XJE9Z5QYHZDJBSNY3VJES8GW8FR0WXVVDH5NFYRNPC5H8TESBFYDQXWQW82G60G1Q8MNSTSWRPWZ9GDZ7P73RQFBKXVNFKVY5T2VH3JEW68PGR1P4MV75ZM4SHN7RTGB0BZQEMRRC9SNDTJ2NF9YCZG8DHES8AYHZKWXQN5GSJBE0MTP316PW23W2RVNJCVCS6TC28J6GGJ660JEET1T1G2YJ0R846NQRK575WT24DR2Q2PK1Q837VE2EN05KQMCD8C4SCDTTVRF4183080NT78B8YY25XM9EHV5M3B82WJY8ADA4VNX7BCNFDRHP9EWHXA0SRTEJJCNMRVW30YD1XYDRZ2VD3SEDE55A3VDTQFHPAZQ8C227RV2WN1H8WD8FSM5WTQXH4PX6R09ERRXWFY15B67JA1W0RGA77BJS5X4D7N9XDJZ8EDE1NHXHNETX71FR7FA979E6ESAE8CGK8SNDYC8Z5920M1G0JESJAYPYZREV826MBMVSMANNFTXNNXEGXJRQD27K12F8W0SNJC8B2C8W4CY9XYNXBBQG6T8TH0M8EB0HR5E36VKYB7A9H7JTPBNB7T24EV9HYX1Q38ZJ6DKABKRQGDMSDHVSHQ2Y77EZVH6KF3XNEGVBACVJ1ZPKSH2E8J963AW6K4S22N3JPMCCTYJZPQT001GCB7E91ZNW82G60G1F5TBF02RRWS5FENV7SVW0P2WXKM4KGZNP0H3T0SP1S8H2P9R5ARQYT1XB44D4C6N9DGXW0ZY9RTPDN7FV2ZZ809EKEVD0PFJNNE7QENVE3AJ9YB4CEQ30APTVY6DJBSE90MXVJ1DT7PG7P404J6A6VCCJVSSCH330X97D8E8GCJV8T3A5TMANN5CAC8M5RR22PJAMP3ZC9XVC","encryption_private_key":"62109F020403038614N8CJ46YW6G20810M0090G4MRR84152080G00M2040G1M43X9T1PGMHQDXM3307ZR8A6E00XHF5FZEAMSTS7RZ3JZVAP50C0FKX592V5AM55E38Q6HXJA8K8SVRYCTDV6CATPTRGPW5NQ3T38CJ77KW0V8H20FBJSB69N51TMKG6MW7GJ1VBR5WY0JJ1VM5Z061SHT0G61S0WNMDZDSEYJP8QC0WW63CDV1HFHYV63TCDQB16PV1SZ53AHB3P2H2CKWQVN8P00RBGS9A6TEA1NZCR8RWXDTB1KBWJWWNW5EV94PCH6YJPVRA0DB12TAC2AR9FP4BDJTZS322Z1ZRDCXMBZXPMRHFVECEPAJGER1DBXM1HWE8NBC7D1MR3RSV9PSRPTXT07WVKH1YRDQ8S1CPCRACXZ953YT2PGESGD2FFQ9JDDR8Q0HJAHTDC8S83K0ZRR20C0G0082G80G0166J2QHM55V7EYGBWJC3288XZ3GC3WR8C36VK5J567WJHTPSMX7PV5JYWPY6YNEAGGXZFN7KHEC20W1NJM50KQWYG51ANKB91GA67GD7184Q0QKZ8BRM3BXFDAV0E1W4ENYGVAQ2TZN8ZBC3PTMNGNB5PG9DCPZADZVAFW8D3N1QMTWD5Q4JAWSHDBTR43AYMQ0NMVH4BSDFCAHKEHVCDPZTNPZ79BS52XPC36B95YDRSWFJ4ZS0VFS4KNQTJBHFXVS017N892FMBX3600TMPPB6QX0XTB03Q2CS755CXERJ75ZGE3G6C01EQZ7W1VKEFKBKF83RZG6TTKTY4PHV7N8CF7NTXA5WF4WDQR60TZG6GG7NQ2S4XF9S8MD3NPKZ80WE7QZGZH5Y59VWVPWG3GJZG82G60G1VWXEXJQZHXZFSQGFA3JWWEB8TFRMGJ9HMRF9GQFX0XB5QQVVTFWK563SG6SR0GZFGXRZY6AS31VM3SN2SFMQS51GWHF9QS8Z40G29CG96NDW5XTTTQEHF8SADQVXYSKEC8WPES3278B1X0NDQSY8B9NJ083X1V1FSVBCKZB62B9M6HCCR3YFFJJVTCBP9DCJ75RXJZF8YYNC4183080VV30KFVNSRC3NTKW174J3MC415EK8MKGNN1J62X235SM49KYBG3MMENBV03FGMWH55QE35PTVKQG48M6VAJHGNNYTGXGT0X7C9YECGC700414Z74D42W08Q53TWC9AGFR035D93CBKYXPQ5RY3CGWPA3RGZTDGDEE4J6PVHCZX6RBYRYQG53XFE7E4VKRM5Q54N62WAY13KW60M1G0XGR76Y416G5ZCF6BEYKRPP183RPH1QV24KMQENQVB8F6KWAZD8YTPZGJGJK1YNAW7HWJDB9BH0QNXFVS0KFF54V6YNKDQ5NN3HT3P8DM00J5B5QBM7SZ3BKD7VYTF498BRMC9639047XVGDJ6ZNWYGSN01HK3J7PAW7ETHM2GH6B67RR3GAS2PMYKW7JS1W0MQKM5DGQJZCG82G602QH54XPJEQ57Z3S5BGWBQVXZ1V26YBB2211KC810DSNSM9FSWZ27F427XN9D3XHKKSJ5R66VNCT7EYSRT0JW7F2HRTXQWKEBJPNKPPTRHPS3TKNKH5TSTA2GCPS39VWQV1ZMDSX42P38550V7VEV7G953MA3Q9VF4613GRVWBGG1PXTDSYFHM1NYKVN98B21P7SRVPQV4ZCCS0A0R0WGQ4AEKG8JFSYPMC03TA8M2J5PN51PVZP6WZ1R167B92AQGM3YTKAK1QECQK1S4T2HV04Y2YJDCYK5AYVJQYXFYQPFP1CYSE3Q458BBN8RXW5QMJEPCTGK2N3H7K5T2ZJMWNJY27J53B08703K93X0JSM643V7Z95CBJNKVZR4QXRKHFPG5W79AKZ3WP9CXY43XXSHG0KN424","authentication_private_key":"62109F820403038614N8CJ46YW6G20810M0090G4MWR84153080G00M2040G1CZR41XW4ANY74SDV2B2ZEFWPR0E9RXRWFAVAXJHZBMSYXHE5F32X4PKDTVBNHVNYAQYXDXMFV9WCYZA16PKDCEWZ87FKKDH8EJP7NQZB5GK83HJ6KX3P04BD878D99QZ44EE6SYK3YJZMBEP9H1755FKR25RBH5VEYRZ9X2T2DTBV34SREH7F7A81KCC4ZWBRWP1EH179T4RH17R278A9834G2FNPAKST9HK7P878W85DW9XG9BJQ40YCDTP6DGACTKZBJ992M6BDJN69HQZ9GDHF73M2CV7C96YYH8YKMD3V3YXNR17PP0BMKAB30T7KGTDMM92XH6Z0CKA6VQSRFFA59H77GESQYT05SY4N2AGRM9468G1XYJ7M2PM3EXZAYM1GJ934PRGG965Z8KRVZEFF820C0G0082G80G05FQQ0106J3KPWETPQ35EPRCTP0K8VTBJTDZ7P6PG6CXM968G5AP2GN6640WVE6D5AB2J4RYSNTVF4X6JJWWSYDCB7JTDFK1W27T7ZMEQB2MA2K57J0S5QPN4PQAF4C19J962JV51YSFNECVDT328F8H05MJF05H76N61QX5VQ2REQ879M02B7QRQ9NH284XCWWTA5T8HSJ6BG70SCF7S0YJX8F3DQDB763X3D4RY8Y7HVPVBCR9JKJC521R2BHR12WYZ329KN38MVJ69NHJMH2BFT76TRQXY8N6ECEGVC9GWC1KCVSGKQE4XSP6ZYFFVFBNTEVR3653PMF9S9XYSJYBPKQBTF2Q4SY2D56B0Z5MN0A0R7YY1PGZN69A9RPT8H18QVQPJR1YTM1BBNTQHS6KC382G60G1R1Q8A547XJHMF7Z9BR8PC9YPKAMCRKMFNFD71MS5P8CY3TC81MYWRBVNBC7DJA7DCFXHTW3BMC0RCZ39SYY2PS29FQJQR7ZDT8H260P4X8Q8D5JYYAJFDBSY82BQY4K71V5PGYH6PS0Q79CRWWQJQ68JHESE2C829CMNXDWTCW21G9SZC6JJ32P6HSGQ1T5K8VD9B6GFPZ6XC183080SNXJNZ2MFM6GRPHEN3SHTNE9M63KWHG4EQBEPYYG7E5DH5Q4NYP19ZGJ192XV7GJFRSH14XB3DXR4GEZACKMMEMSFTFJ16NQXMYYXTRF4KY9NPYPSD3E65RMF9CDGWAGMT46RTWZE2PRSSRFS15Z0CQ0HCN5K9EG16GGE77E27DNN43XFD2G3YKYXAMDPF7CGT3K8V3T328ZE0M1G1SM8XBMTY9SWK7MXZWVR8SM5SBWDXZDC72DSYWFDBNJ5Y0D3NMVKVPRDGJ23SQM7H0JSXAY8SPBM8D5NNAKD6SRRRHN8VYSHMX2S21FB0594DRA8G8VY4D1XRR90G4VNHF06BSHWVPRCPC1R68ZPW0BGBBE3ZHWN344AD0HM7GCA3N8B0276YZHQRJ2YAZ4RKG7Q8V4VJ8Q9682G60G1F9CJZV9E4J9W42PBKY5GD66FHBYDPKAH54MQ5BX7VFXES4ZAR46BM6MRBM90FQQ7DRVXGVGER6NWJK2195XJXMKQHW7B36HGT12HK1QQFF1J7RB6QWJEEFXR2M7M4YK3CS00RRMYA5XKVNNPV2VZNBXKGJ5SCHGSN6GKDNXE0HSM2VMBKS794DGB7D4PYSEYKSB9BM57G1B4418303W3D38F61CC4RC8DNDZNH9S7N0E4AM97QVRPMY7FERHN0JM4VHD5ET49RYJVW9Y9GBREB801N4A70Z9EGCBM2WJZYSF9KXX8EVDFX37KDJ2G4MTJNR628QV206M1QJ4P4EKS04D9GB8TTQNHJCXJ1PX9PJ44WYGAPKQQP5ENN1VCJAHVXTD5BXHJZYASWZ8QZJY7V760W36ER0","submitted_ini":true,"submitted_hia":true} diff --git a/nixos/tests/taler/conf/exchange-account.json b/nixos/tests/taler/conf/exchange-account.json new file mode 100644 index 000000000000..4bc476ff894f --- /dev/null +++ b/nixos/tests/taler/conf/exchange-account.json @@ -0,0 +1,16 @@ +[ + { + "operation": "exchange-enable-wire-0", + "arguments": { + "payto_uri": "payto://x-taler-bank/bank:8082/exchange?receiver-name=Exchange", + "debit_restrictions": [], + "credit_restrictions": [], + "priority": 0, + "validity_start": { + "t_s": 1725886541 + }, + "master_sig_add": "68WDT3JX1S5GQ9D3RZWXQVZK9AHFZ46YY5DA993720YA3SCBR4SW3X09NH5DECTXGWBKSN0MGKE1ANA9QZ95SKSNYPS9T9G46PCJC20", + "master_sig_wire": "39CEN9007DEXXMSDFZX1R2YYNANZYAFHX4EZC4ZX3C8DQEYT83JNVCVYMWYDGWEX6S891ZPXD6QHJE9J41YV9EN703Q0NM0MVE4FP18" + } + } +] diff --git a/nixos/tests/taler/conf/private.key b/nixos/tests/taler/conf/private.key new file mode 100644 index 000000000000..3025c97740ad --- /dev/null +++ b/nixos/tests/taler/conf/private.key @@ -0,0 +1 @@ +ƒØ'IÕ‚–v&©ˆû¾¢¶ßH{VWýrƒ¶CjÜ>¦ \ No newline at end of file diff --git a/nixos/tests/taler/conf/taler-accounts.conf b/nixos/tests/taler/conf/taler-accounts.conf new file mode 100644 index 000000000000..9246c25a83ad --- /dev/null +++ b/nixos/tests/taler/conf/taler-accounts.conf @@ -0,0 +1,10 @@ +[exchange-account-test] +PAYTO_URI = payto://x-taler-bank/bank:8082/exchange?receiver-name=Exchange +ENABLE_DEBIT = YES +ENABLE_CREDIT = YES + +[exchange-accountcredentials-test] +WIRE_GATEWAY_URL = http://bank:8082/accounts/exchange/taler-wire-gateway/ +WIRE_GATEWAY_AUTH_METHOD = BASIC +USERNAME = exchange +PASSWORD = exchange diff --git a/nixos/tests/taler/conf/taler-denominations.conf b/nixos/tests/taler/conf/taler-denominations.conf new file mode 100644 index 000000000000..e0c46ba9d6f6 --- /dev/null +++ b/nixos/tests/taler/conf/taler-denominations.conf @@ -0,0 +1,95 @@ +[COIN-KUDOS-n1-t1726827661] +VALUE = KUDOS:0.1 +DURATION_WITHDRAW = 7 days +DURATION_SPEND = 2 years +DURATION_LEGAL = 6 years +FEE_WITHDRAW = KUDOS:0 +FEE_DEPOSIT = KUDOS:0 +FEE_REFRESH = KUDOS:0 +FEE_REFUND = KUDOS:0 +RSA_KEYSIZE = 2048 +CIPHER = RSA + +[COIN-KUDOS-n2-t1726827661] +VALUE = KUDOS:0.2 +DURATION_WITHDRAW = 7 days +DURATION_SPEND = 2 years +DURATION_LEGAL = 6 years +FEE_WITHDRAW = KUDOS:0 +FEE_DEPOSIT = KUDOS:0 +FEE_REFRESH = KUDOS:0 +FEE_REFUND = KUDOS:0 +RSA_KEYSIZE = 2048 +CIPHER = RSA + +[COIN-KUDOS-n3-t1726827661] +VALUE = KUDOS:0.4 +DURATION_WITHDRAW = 7 days +DURATION_SPEND = 2 years +DURATION_LEGAL = 6 years +FEE_WITHDRAW = KUDOS:0 +FEE_DEPOSIT = KUDOS:0 +FEE_REFRESH = KUDOS:0 +FEE_REFUND = KUDOS:0 +RSA_KEYSIZE = 2048 +CIPHER = RSA + +[COIN-KUDOS-n4-t1726827661] +VALUE = KUDOS:0.8 +DURATION_WITHDRAW = 7 days +DURATION_SPEND = 2 years +DURATION_LEGAL = 6 years +FEE_WITHDRAW = KUDOS:0 +FEE_DEPOSIT = KUDOS:0 +FEE_REFRESH = KUDOS:0 +FEE_REFUND = KUDOS:0 +RSA_KEYSIZE = 2048 +CIPHER = RSA + +[COIN-KUDOS-n5-t1726827661] +VALUE = KUDOS:1.6 +DURATION_WITHDRAW = 7 days +DURATION_SPEND = 2 years +DURATION_LEGAL = 6 years +FEE_WITHDRAW = KUDOS:0 +FEE_DEPOSIT = KUDOS:0 +FEE_REFRESH = KUDOS:0 +FEE_REFUND = KUDOS:0 +RSA_KEYSIZE = 2048 +CIPHER = RSA + +[COIN-KUDOS-n6-t1726827661] +VALUE = KUDOS:3.2 +DURATION_WITHDRAW = 7 days +DURATION_SPEND = 2 years +DURATION_LEGAL = 6 years +FEE_WITHDRAW = KUDOS:0 +FEE_DEPOSIT = KUDOS:0 +FEE_REFRESH = KUDOS:0 +FEE_REFUND = KUDOS:0 +RSA_KEYSIZE = 2048 +CIPHER = RSA + +[COIN-KUDOS-n7-t1726827661] +VALUE = KUDOS:6.4 +DURATION_WITHDRAW = 7 days +DURATION_SPEND = 2 years +DURATION_LEGAL = 6 years +FEE_WITHDRAW = KUDOS:0 +FEE_DEPOSIT = KUDOS:0 +FEE_REFRESH = KUDOS:0 +FEE_REFUND = KUDOS:0 +RSA_KEYSIZE = 2048 +CIPHER = RSA + +[COIN-KUDOS-n8-t1726827661] +VALUE = KUDOS:12.8 +DURATION_WITHDRAW = 7 days +DURATION_SPEND = 2 years +DURATION_LEGAL = 6 years +FEE_WITHDRAW = KUDOS:0 +FEE_DEPOSIT = KUDOS:0 +FEE_REFRESH = KUDOS:0 +FEE_REFUND = KUDOS:0 +RSA_KEYSIZE = 2048 +CIPHER = RSA diff --git a/nixos/tests/taler/default.nix b/nixos/tests/taler/default.nix new file mode 100644 index 000000000000..00ce30ded6b6 --- /dev/null +++ b/nixos/tests/taler/default.nix @@ -0,0 +1,7 @@ +{ + system ? builtins.currentSystem, + pkgs ? import ../../.. { inherit system; }, +}: +{ + basic = import ./tests/basic.nix { inherit system pkgs; }; +} diff --git a/nixos/tests/taler/tests/basic.nix b/nixos/tests/taler/tests/basic.nix new file mode 100644 index 000000000000..3eeb50b04fa9 --- /dev/null +++ b/nixos/tests/taler/tests/basic.nix @@ -0,0 +1,270 @@ +import ../../make-test-python.nix ( + { pkgs, lib, ... }: + let + cfgNodes = pkgs.callPackage ../common/nodes.nix { inherit lib; }; + in + { + # NOTE: The Nexus conversion subtest requires internet access, so to run it + # you must run the test with: + # - nix run .#nixosTests.taler.basic.driver + # or interactively: + # - nix run .#nixosTests.taler.basic.driverInteractive + # - run_tests() + + name = "GNU Taler Basic Test"; + meta = { + maintainers = lib.teams.ngi.members; + }; + + # Taler components virtual-machine nodes + nodes = { + inherit (cfgNodes.nodes) + bank + client + exchange + merchant + ; + }; + + # TODO: make tests for each component? + testScript = + { nodes, ... }: + let + cfgScripts = pkgs.callPackage ../common/scripts.nix { inherit lib pkgs nodes; }; + + inherit (cfgNodes) CURRENCY FIAT_CURRENCY; + inherit (cfgScripts) commonScripts; + + bankConfig = nodes.bank.config.environment.etc."libeufin/libeufin.conf".source; + bankSettings = nodes.bank.services.libeufin.settings.libeufin-bank; + nexusSettings = nodes.bank.services.libeufin.nexus.settings; + + # Bank admin account credentials + AUSER = "admin"; + APASS = "admin"; + + TUSER = "testUser"; + TPASS = "testUser"; + + exchangeAccount = ../conf/exchange-account.json; + in + + # python + '' + import json + + # import common scripts + ${commonScripts} + + + # NOTE: start components up individually so they don't conflict before their setup is done + bank.start() + client.start() + bank.wait_for_open_port(8082) + + with subtest("Set up Libeufin bank"): + # Modify admin account password, increase debit threshold + systemd_run(bank, 'libeufin-bank passwd -c "${bankConfig}" "${AUSER}" "${APASS}"', "libeufin-bank") + systemd_run(bank, 'libeufin-bank edit-account -c ${bankConfig} --debit_threshold="${bankSettings.CURRENCY}:1000000" ${AUSER}', "libeufin-bank") + + # NOTE: the exchange is enabled before the bank starts using the `initialAccounts` option + # TODO: just use that option instead of this? + with subtest("Register bank accounts"): + # username, password, name + register_bank_account("testUser", "testUser", "User") + register_bank_account("merchant", "merchant", "Merchant") + + + exchange.start() + exchange.wait_for_open_port(8081) + + + with subtest("Set up exchange"): + exchange.wait_until_succeeds("taler-exchange-offline download sign upload") + # Enable exchange wire account + exchange.succeed('taler-exchange-offline upload < ${exchangeAccount}') + + # NOTE: cannot deposit coins/pay merchant if wire fees are not set up + exchange.succeed('taler-exchange-offline wire-fee now x-taler-bank "${CURRENCY}:0" "${CURRENCY}:0" upload') + exchange.succeed('taler-exchange-offline global-fee now "${CURRENCY}:0" "${CURRENCY}:0" "${CURRENCY}:0" 1h 6a 0 upload') + + + # Verify that exchange keys exist + bank.succeed("curl -s http://exchange:8081/keys") + + + merchant.start() + merchant.wait_for_open_port(8083) + + + with subtest("Set up merchant"): + # Create default instance (similar to admin) + succeed(merchant, [ + "curl -X POST", + "-H 'Authorization: Bearer secret-token:super_secret'", + """ + --data '{ + "auth": { "method": "external" }, + "id": "default", + "name": "default", + "user_type": "business", + "address": {}, + "jurisdiction": {}, + "use_stefan": true, + "default_wire_transfer_delay": { "d_us": 3600000000 }, + "default_pay_delay": { "d_us": 3600000000 } + }' + """, + "-sSfL 'http://merchant:8083/management/instances'" + ]) + # Register bank account address + succeed(merchant, [ + "curl -X POST", + "-H 'Content-Type: application/json'", + """ + --data '{ + "payto_uri": "payto://x-taler-bank/bank:8082/merchant?receiver-name=Merchant", + "credit_facade_url": "http://bank:8082/accounts/merchant/taler-revenue/", + "credit_facade_credentials":{"type":"basic","username":"merchant","password":"merchant"} + }' + """, + "-sSfL 'http://merchant:8083/private/accounts'" + ]) + # Register a new product to be ordered + succeed(merchant, [ + "curl -X POST", + "-H 'Content-Type: application/json'", + """ + --data '{ + "product_id": "1", + "description": "Product with id 1 and price 1", + "price": "${CURRENCY}:1", + "total_stock": 20, + "unit": "packages", + "next_restock": { "t_s": "never" } + }' + """, + "-sSfL 'http://merchant:8083/private/products'" + ]) + + + client.succeed("curl -s http://exchange:8081/") + + + # Make a withdrawal from the CLI wallet + with subtest("Make a withdrawal from the CLI wallet"): + balanceWanted = "${CURRENCY}:10" + + # Register exchange + with subtest("Register exchange"): + wallet_cli("exchanges add http://exchange:8081/") + wallet_cli("exchanges accept-tos http://exchange:8081/") + + # Request withdrawal from the bank + withdrawal = json.loads( + succeed(client, [ + "curl -X POST", + "-u ${TUSER}:${TPASS}", + "-H 'Content-Type: application/json'", + f"""--data '{{"amount": "{balanceWanted}"}}'""", # double brackets escapes them + "-sSfL 'http://bank:8082/accounts/${TUSER}/withdrawals'" + ]) + ) + + # Accept & confirm withdrawal + with subtest("Accept & confirm withdrawal"): + wallet_cli(f"withdraw accept-uri {withdrawal["taler_withdraw_uri"]} --exchange http://exchange:8081/") + succeed(client, [ + "curl -X POST", + "-u ${TUSER}:${TPASS}", + "-H 'Content-Type: application/json'", + f"-sSfL 'http://bank:8082/accounts/${TUSER}/withdrawals/{withdrawal["withdrawal_id"]}/confirm'" + ]) + + # Process transactions + wallet_cli("run-until-done") + + verify_balance(balanceWanted) + + + with subtest("Pay for an order"): + balanceWanted = "${CURRENCY}:9" # after paying + + # Create an order to be paid + response = json.loads( + succeed(merchant, [ + "curl -X POST", + "-H 'Content-Type: application/json'", + """ + --data '{ + "order": { "amount": "${CURRENCY}:1", "summary": "Test Order" }, + "inventory_products": [{ "product_id": "1", "quantity": 1 }] + }' + """, + "-sSfL 'http://merchant:8083/private/orders'" + ]) + ) + order_id = response["order_id"] + token = response["token"] + + # Get order pay URI + response = json.loads( + succeed(merchant, [ + "curl -sSfL", + f"http://merchant:8083/private/orders/{order_id}" + ]) + ) + wallet_cli("run-until-done") + + # Process transaction + wallet_cli(f"""handle-uri -y '{response["taler_pay_uri"]}'""") + wallet_cli("run-until-done") + + verify_balance(balanceWanted) + + # Only run Nexus conversion test if ebics server is available + (status, out) = bank.execute('curl -sSfL "${nexusSettings.nexus-ebics.HOST_BASE_URL}"') + + if status != 0: + bank.log("Can't connect to ebics server. Skipping Nexus conversion subtest.") + else: + with subtest("Libeufin Nexus currency conversion"): + regionalWanted = "20" + + # Setup Nexus ebics keys + systemd_run(bank, "libeufin-nexus ebics-setup -L debug -c /etc/libeufin/libeufin.conf", "libeufin-nexus") + + # Set currency conversion rates (1:1) + succeed(bank, [ + "curl -X POST", + "-H 'Content-Type: application/json'", + "-u ${AUSER}:${APASS}", + """ + --data '{ + "cashin_ratio": "1", + "cashin_fee": "${CURRENCY}:0", + "cashin_tiny_amount": "${CURRENCY}:0.01", + "cashin_rounding_mode": "nearest", + "cashin_min_amount": "${FIAT_CURRENCY}:1", + "cashout_ratio": "1", + "cashout_fee": "${FIAT_CURRENCY}:0", + "cashout_tiny_amount": "${FIAT_CURRENCY}:0.01", + "cashout_rounding_mode": "nearest", + "cashout_min_amount": "${CURRENCY}:1" + }' + """, + "-sSfL 'http://bank:8082/conversion-info/conversion-rate'" + ]) + + # Make fake transaction (we only need reservePub) + response = wallet_cli("""api 'acceptManualWithdrawal' '{ "exchangeBaseUrl":"http://exchange:8081/", "amount":"${CURRENCY}:5" }'""") + reservePub = json.loads(response)["result"]["reservePub"] + + # Convert fiat currency to regional + systemd_run(bank, f"""libeufin-nexus testing fake-incoming -c ${bankConfig} --amount="${FIAT_CURRENCY}:{regionalWanted}" --subject="{reservePub}" "payto://iban/CH4740123RW4167362694" """, "libeufin-nexus") + wallet_cli("run-until-done") + + verify_conversion(regionalWanted) + ''; + } +)