/* This file is part of TALER (C) 2014-2017, 2020 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file merchant/test_merchantdb.c * @brief testcase for merchant's postgres db plugin * @author Marcello Stanisci * @author Christian Grothoff */ #include "platform.h" #include #include #include #include "taler_merchantdb_lib.h" /** * Global return value for the test. Initially -1, set to 0 upon * completion. Other values indicate some kind of error. */ static int result; /** * Handle to the plugin we are testing. */ static struct TALER_MERCHANTDB_Plugin *plugin; #define TEST_WITH_FAIL_CLAUSE(test, on_fail) \ if (0 != (test)) \ { \ on_fail \ } #define TEST_COND_RET_ON_FAIL(cond, msg) \ if (! (cond)) \ { \ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ msg); \ return 1; \ } #define TEST_RET_ON_FAIL(__test) \ TEST_WITH_FAIL_CLAUSE (__test, \ return 1; \ ) /* ********** Instances ********** */ /** * Container for instance settings along with keys. */ struct InstanceData { /** * The instance settings. */ struct TALER_MERCHANTDB_InstanceSettings instance; /** * The public key for the instance. */ struct TALER_MerchantPublicKeyP merchant_pub; /** * The private key for the instance. */ struct TALER_MerchantPrivateKeyP merchant_priv; }; /** * Creates data for an instance to use with testing. * * @param instance_id the identifier for this instance. * @param instance the instance data to be filled. */ static void make_instance (char *instance_id, struct InstanceData *instance) { GNUNET_CRYPTO_eddsa_key_create (&instance->merchant_priv.eddsa_priv); GNUNET_CRYPTO_eddsa_key_get_public (&instance->merchant_priv.eddsa_priv, &instance->merchant_pub.eddsa_pub); instance->instance.id = instance_id; instance->instance.name = "Test"; instance->instance.address = json_array (); GNUNET_assert (NULL != instance->instance.address); GNUNET_assert (0 == json_array_append_new (instance->instance.address, json_string ("123 Example St"))); instance->instance.jurisdiction = json_array (); GNUNET_assert (NULL != instance->instance.jurisdiction); GNUNET_assert (0 == json_array_append_new (instance->instance.jurisdiction, json_string ("Ohio"))); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:1200.40", &instance->instance. default_max_deposit_fee)); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:1200.40", &instance->instance. default_max_wire_fee)); instance->instance.default_wire_fee_amortization = 1; instance->instance.default_wire_transfer_delay = GNUNET_TIME_relative_get_minute_ (); instance->instance.default_pay_delay = GNUNET_TIME_relative_get_second_ (); } /** * Frees memory allocated when creating an instance for testing. * * @param instance the instance containing the memory to be freed. */ static void free_instance_data (struct InstanceData *instance) { json_decref (instance->instance.address); json_decref (instance->instance.jurisdiction); } /** * Creates an account with test data for an instance. * * @param account the account to initialize. */ static void make_account (struct TALER_MERCHANTDB_AccountDetails *account) { GNUNET_CRYPTO_hash_create_random (GNUNET_CRYPTO_QUALITY_STRONG, &account->h_wire); GNUNET_CRYPTO_hash_create_random (GNUNET_CRYPTO_QUALITY_STRONG, &account->salt); account->payto_uri = "payto://x-taler-bank/bank.demo.taler.net/4"; account->active = true; } /** * Instance settings along with corresponding accounts. */ struct InstanceWithAccounts { /** * Pointer to the instance settings. */ const struct TALER_MERCHANTDB_InstanceSettings *instance; /** * Length of the array of accounts. */ unsigned int accounts_length; /** * Pointer to the array of accounts. */ const struct TALER_MERCHANTDB_AccountDetails *accounts; }; /** * Closure for testing instance lookup. */ struct TestLookupInstances_Closure { /** * Number of instances to compare to. */ unsigned int instances_to_cmp_length; /** * Pointer to array of instances. */ const struct InstanceWithAccounts *instances_to_cmp; /** * Pointer to array of number of matches for each instance. */ unsigned int *results_matching; /** * Total number of results returned. */ unsigned int results_length; }; /** * Compares two instances for equality. * * @param a the first instance. * @param b the second instance. * @return 0 on equality, 1 otherwise. */ static int check_instances_equal (const struct TALER_MERCHANTDB_InstanceSettings *a, const struct TALER_MERCHANTDB_InstanceSettings *b) { if ((0 != strcmp (a->id, b->id)) || (0 != strcmp (a->name, b->name)) || (1 != json_equal (a->address, b->address)) || (1 != json_equal (a->jurisdiction, b->jurisdiction)) || (GNUNET_OK != TALER_amount_cmp_currency (&a->default_max_deposit_fee, &b->default_max_deposit_fee)) || (0 != TALER_amount_cmp (&a->default_max_deposit_fee, &b->default_max_deposit_fee)) || (GNUNET_OK != TALER_amount_cmp_currency (&a->default_max_wire_fee, &b->default_max_wire_fee)) || (0 != TALER_amount_cmp (&a->default_max_wire_fee, &b->default_max_wire_fee)) || (a->default_wire_fee_amortization != b->default_wire_fee_amortization) || (a->default_wire_transfer_delay.rel_value_us != b->default_wire_transfer_delay.rel_value_us) || (a->default_pay_delay.rel_value_us != b->default_pay_delay.rel_value_us)) return 1; return 0; } /** * Compares two accounts for equality. * * @param a the first account. * @param b the second account. * @return 0 on equality, 1 otherwise. */ static int check_accounts_equal (const struct TALER_MERCHANTDB_AccountDetails *a, const struct TALER_MERCHANTDB_AccountDetails *b) { if ((0 != GNUNET_CRYPTO_hash_cmp (&a->h_wire, &b->h_wire)) || (0 != GNUNET_CRYPTO_hash_cmp (&a->salt, &b->salt)) || (0 != strcmp (a->payto_uri, b->payto_uri)) || (a->active != b->active)) return 1; return 0; } /** * Called after testing 'lookup_instances'. * * @param cls pointer to 'struct TestLookupInstances_Closure'. * @param merchant_pub public key of the instance * @param merchant_priv private key of the instance, NULL if not available * @param is general instance settings * @param accounts_length length of the @a accounts array * @param accounts list of accounts of the merchant */ static void lookup_instances_cb (void *cls, const struct TALER_MerchantPublicKeyP *merchant_pub, const struct TALER_MerchantPrivateKeyP *merchant_priv, const struct TALER_MERCHANTDB_InstanceSettings *is, unsigned int accounts_length, const struct TALER_MERCHANTDB_AccountDetails accounts[]) { struct TestLookupInstances_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; /* Look through the closure and test each instance for equality */ for (unsigned int i = 0; cmp->instances_to_cmp_length > i; ++i) { int accounts_matching[accounts_length]; bool accounts_match = true; if (0 != check_instances_equal (cmp->instances_to_cmp[i].instance, is)) continue; if (accounts_length != cmp->instances_to_cmp[i].accounts_length) continue; /* Count matches between the accounts found and accounts in cls */ for (unsigned int j = 0; accounts_length > j; ++j) accounts_matching[j] = 0; for (unsigned int j = 0; accounts_length > j; ++j) { for (unsigned int k = 0; accounts_length > k; ++k) { if (0 == check_accounts_equal (&cmp->instances_to_cmp[i].accounts[j], &accounts[k])) accounts_matching[j] += 1; } } /* Each account from the lookup should match with one and only one from cls */ for (unsigned int j = 0; accounts_length > j; ++j) if (1 != accounts_matching[j]) accounts_match = false; if (true == accounts_match) cmp->results_matching[i] += 1; } } /** * Tests @e insert_instance. * * @param instance the instance data to insert. * @param expected_result the result that should be returned from the plugin. * @return 0 on success, 1 on failure. */ static int test_insert_instance (const struct InstanceData *instance, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_instance (plugin->cls, &instance->merchant_pub, &instance->merchant_priv, &instance->instance), "Insert instance failed\n"); return 0; } /** * Tests @e update_instance. * * @param updated_data the instance data to update the row in the database to. * @param expected_result the result that should be returned from the plugin. * @return 0 on success, 1 on failure. */ static int test_update_instance (const struct InstanceData *updated_data, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->update_instance (plugin->cls, &updated_data->instance), "Update instance failed\n"); return 0; } /** * Tests @e lookup_instances. * * @param active_only whether to lookup all instance, or only active ones. * @param instances_length number of instances to compare to in @e instances. * @param instances a list of instances that will be compared with the results * found. * @return 0 on success, 1 otherwise. */ static int test_lookup_instances (bool active_only, unsigned int instances_length, struct InstanceWithAccounts instances[]) { unsigned int results_matching[instances_length]; struct TestLookupInstances_Closure cmp = { .instances_to_cmp_length = instances_length, .instances_to_cmp = instances, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * instances_length); if (0 > plugin->lookup_instances (plugin->cls, active_only, &lookup_instances_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup instances failed\n"); return 1; } if (instances_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup instances failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; instances_length > i; ++i) { if (1 != cmp.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup instances failed: mismatched data\n"); return 1; } } return 0; } /** * Tests removing the private key of the given instance from the database. * * @param instance the instance whose private key to delete. * @param expected_result the result we expect the db to return. * @return 0 on success, 1 otherwise. */ static int test_delete_instance_private_key (const struct InstanceData *instance, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->delete_instance_private_key (plugin->cls, instance->instance .id), "Delete instance private key failed\n"); return 0; } /** * Tests inserting an account for a merchant instance. * * @param instance the instance to associate the account with. * @param account the account to insert. * @param expected_result the result expected from the db. * @return 0 on success, 1 otherwise. */ static int test_insert_account (const struct InstanceData *instance, const struct TALER_MERCHANTDB_AccountDetails *account, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_account (plugin->cls, instance->instance.id, account), "Insert account failed\n"); return 0; } /** * Tests deactivating an account. * * @param account the account to inactivate. * @param expected_result the result expected from the plugin. * @return 0 on success, 1 otherwise. */ static int test_inactivate_account (const struct TALER_MERCHANTDB_AccountDetails *account, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->inactivate_account (plugin->cls, &account->h_wire), "Inactivate account failed\n"); return 0; } /** * Closure for instance tests */ struct TestInstances_Closure { /** * The list of instances that we use for testing instances. */ struct InstanceData instances[2]; /** * The list of accounts to use with the instances. */ struct TALER_MERCHANTDB_AccountDetails accounts[2]; }; /** * Sets up the data structures used in the instance tests * * @cls the closure to initialize with test data. */ static void pre_test_instances (struct TestInstances_Closure *cls) { /* Instance */ make_instance ("test_instances_inst0", &cls->instances[0]); make_instance ("test_instances_inst1", &cls->instances[1]); /* Accounts */ make_account (&cls->accounts[0]); make_account (&cls->accounts[1]); } /** * Handles all teardown after testing * * @cls the closure to free data from */ static void post_test_instances (struct TestInstances_Closure *cls) { free_instance_data (&cls->instances[0]); free_instance_data (&cls->instances[1]); } /** * Function that tests instances. * * @param cls closure with config * @return 0 on success, 1 if failure. */ static int run_test_instances (struct TestInstances_Closure *cls) { struct InstanceWithAccounts instances[2] = { { .accounts_length = 0, .accounts = cls->accounts, .instance = &cls->instances[0].instance }, { .accounts_length = 0, .accounts = cls->accounts, .instance = &cls->instances[1].instance } }; uint64_t account_serial; /* Test inserting an instance */ TEST_RET_ON_FAIL (test_insert_instance (&cls->instances[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double insertion fails */ TEST_RET_ON_FAIL (test_insert_instance (&cls->instances[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test lookup instances- is our new instance there? */ TEST_RET_ON_FAIL (test_lookup_instances (false, 1, instances)); /* Test update instance */ cls->instances[0].instance.name = "Test - updated"; json_array_append_new (cls->instances[0].instance.address, json_pack ("{s:s, s:I}", "this", "is", "more data", 47)); json_array_append_new (cls->instances[0].instance.jurisdiction, json_pack ("{s:s}", "vegetables", "bad")); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:0.04", &cls->instances[0].instance. default_max_deposit_fee)); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:1.23", &cls->instances[0].instance. default_max_wire_fee)); cls->instances[0].instance.default_wire_fee_amortization = 2; cls->instances[0].instance.default_wire_transfer_delay = GNUNET_TIME_UNIT_HOURS; cls->instances[0].instance.default_pay_delay = GNUNET_TIME_UNIT_MINUTES; TEST_RET_ON_FAIL (test_update_instance (&cls->instances[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_instances (false, 1, instances)); TEST_RET_ON_FAIL (test_update_instance (&cls->instances[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test account creation */ TEST_RET_ON_FAIL (test_insert_account (&cls->instances[0], &cls->accounts[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double account insertion fails */ TEST_RET_ON_FAIL (test_insert_account (&cls->instances[1], &cls->accounts[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); TEST_RET_ON_FAIL (test_insert_account (&cls->instances[0], &cls->accounts[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); instances[0].accounts_length = 1; TEST_RET_ON_FAIL (test_lookup_instances (false, 1, instances)); /* Test inactivate account */ TEST_RET_ON_FAIL (test_inactivate_account (&cls->accounts[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_inactivate_account (&cls->accounts[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); cls->accounts[0].active = false; TEST_RET_ON_FAIL (test_lookup_instances (false, 1, instances)); /* Test lookup account */ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->lookup_account (plugin->cls, cls->instances[0].instance.id, cls->accounts[0].payto_uri, &account_serial)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup account failed\n"); return 1; } if (1 != account_serial) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup account failed: incorrect serial number found\n"); return 1; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->lookup_account (plugin->cls, cls->instances[0].instance.id, "payto://other-uri", &account_serial)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup account failed: account found where there is none\n"); return 1; } /* Test instance private key deletion */ TEST_RET_ON_FAIL (test_delete_instance_private_key (&cls->instances[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_delete_instance_private_key (&cls->instances[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); TEST_RET_ON_FAIL (test_lookup_instances (true, 0, NULL)); TEST_RET_ON_FAIL (test_lookup_instances (false, 1, instances)); TEST_RET_ON_FAIL (test_insert_instance (&cls->instances[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_instances (false, 2, instances)); TEST_RET_ON_FAIL (test_lookup_instances (true, 1, &instances[1])); return 0; } /** * Function that tests instances. * * @return 0 on success, 1 otherwise. */ static int test_instances () { struct TestInstances_Closure test_cls; pre_test_instances (&test_cls); int test_result = run_test_instances (&test_cls); post_test_instances (&test_cls); return test_result; } /* *********** Products ********** */ /** * Frees memory associated with a product. * * @param pd the product containing the memory to free. */ static void free_product (struct TALER_MERCHANTDB_ProductDetails *pd) { if (NULL != pd->description) GNUNET_free (pd->description); if (NULL != pd->description_i18n) json_decref (pd->description_i18n); if (NULL != pd->unit) GNUNET_free (pd->unit); if (NULL != pd->image) json_decref (pd->image); if (NULL != pd->address) json_decref (pd->address); } /** * A container for data relevant to a product. */ struct ProductData { /** * The identifier of the product. */ const char *id; /** * The details of the product. */ struct TALER_MERCHANTDB_ProductDetails product; }; /** * Creates a product for testing with. * * @param id the id of the product. * @param product the product data to fill. */ static void make_product (const char *id, struct ProductData *product) { product->id = id; product->product.description = "This is a test product"; product->product.description_i18n = json_array (); GNUNET_assert (NULL != product->product.description_i18n); product->product.unit = "boxes"; GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:120.40", &product->product.price)); product->product.taxes = json_array (); GNUNET_assert (NULL != product->product.taxes); product->product.total_stock = 55; product->product.total_sold = 0; product->product.total_lost = 0; product->product.image = json_array (); GNUNET_assert (NULL != product->product.image); product->product.address = json_array (); GNUNET_assert (NULL != product->product.address); product->product.next_restock = GNUNET_TIME_absolute_get_zero_ (); } /** * Frees memory associated with @e ProductData. * * @param product the container to free. */ static void free_product_data (struct ProductData *product) { json_decref (product->product.description_i18n); json_decref (product->product.taxes); json_decref (product->product.image); json_decref (product->product.address); } /** * Compare two products for equality. * * @param a the first product. * @param b the second product. * @return 0 on equality, 1 otherwise. */ static int check_products_equal (const struct TALER_MERCHANTDB_ProductDetails *a, const struct TALER_MERCHANTDB_ProductDetails *b) { if ((0 != strcmp (a->description, b->description)) || (1 != json_equal (a->description_i18n, b->description_i18n)) || (0 != strcmp (a->unit, b->unit)) || (GNUNET_OK != TALER_amount_cmp_currency (&a->price, &b->price)) || (0 != TALER_amount_cmp (&a->price, &b->price)) || (1 != json_equal (a->taxes, b->taxes)) || (a->total_stock != b->total_stock) || (a->total_sold != b->total_sold) || (a->total_lost != b->total_lost) || (1 != json_equal (a->image, b->image)) || (1 != json_equal (a->address, b->address)) || (a->next_restock.abs_value_us != b->next_restock.abs_value_us)) return 1; return 0; } /** * Tests inserting product data into the database. * * @param instance the instance to insert the product for. * @param product the product data to insert. * @param expected_result the result we expect the db to return. * @return 0 when successful, 1 otherwise. */ static int test_insert_product (const struct InstanceData *instance, const struct ProductData *product, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_product (plugin->cls, instance->instance.id, product->id, &product->product), "Insert product failed\n"); return 0; } /** * Tests updating product data in the database. * * @param instance the instance to update the product for. * @param product the product data to update. * @param expected_result the result we expect the db to return. * @return 0 when successful, 1 otherwise. */ static int test_update_product (const struct InstanceData *instance, const struct ProductData *product, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->update_product (plugin->cls, instance->instance.id, product->id, &product->product), "Update product failed\n"); return 0; } /** * Tests looking up a product from the db. * * @param instance the instance to query from. * @param product the product to query and compare to. * @return 0 when successful, 1 otherwise. */ static int test_lookup_product (const struct InstanceData *instance, const struct ProductData *product) { struct TALER_MERCHANTDB_ProductDetails lookup_result; if (0 > plugin->lookup_product (plugin->cls, instance->instance.id, product->id, &lookup_result)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup product failed\n"); free_product (&lookup_result); return 1; } const struct TALER_MERCHANTDB_ProductDetails *to_cmp = &product->product; if (0 != check_products_equal (&lookup_result, to_cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup product failed: incorrect product returned\n"); free_product (&lookup_result); return 1; } free_product (&lookup_result); return 0; } /** * Closure for testing product lookup */ struct TestLookupProducts_Closure { /** * Number of product ids to compare to */ unsigned int products_to_cmp_length; /** * Pointer to array of product ids */ const struct ProductData *products_to_cmp; /** * Pointer to array of number of matches for each product */ unsigned int *results_matching; /** * Total number of results returned */ unsigned int results_length; }; /** * Function called after calling @e test_lookup_products * * @param cls a pointer to the lookup closure. * @param product_id the identifier of the product found. */ static void lookup_products_cb (void *cls, const char *product_id) { struct TestLookupProducts_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; cmp->products_to_cmp_length > i; ++i) { if (0 == strcmp (cmp->products_to_cmp[i].id, product_id)) cmp->results_matching[i] += 1; } } /** * Tests looking up all products for an instance. * * @param instance the instance to query from. * @param products_length the number of products we are expecting. * @param products the list of products that we expect to be found. * @return 0 when successful, 1 otherwise. */ static int test_lookup_products (const struct InstanceData *instance, unsigned int products_length, const struct ProductData *products) { unsigned int results_matching[products_length]; struct TestLookupProducts_Closure cls = { .products_to_cmp_length = products_length, .products_to_cmp = products, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * products_length); if (0 > plugin->lookup_products (plugin->cls, instance->instance.id, &lookup_products_cb, &cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup products failed\n"); return 1; } if (products_length != cls.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup products failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; products_length > i; ++i) { if (1 != cls.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup products failed: mismatched data\n"); return 1; } } return 0; } /** * Tests deleting a product. * * @param instance the instance to delete the product from. * @param product the product that should be deleted. * @param expected_result the result that we expect the plugin to return. * @return 0 when successful, 1 otherwise. */ static int test_delete_product (const struct InstanceData *instance, const struct ProductData *product, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->delete_product (plugin->cls, instance->instance.id, product->id), "Delete product failed\n"); return 0; } /** * Closure for product tests. */ struct TestProducts_Closure { /** * The instance to use for this test. */ struct InstanceData instance; /** * The array of products. */ struct ProductData products[2]; }; /** * Sets up the data structures used in the product tests. * * @param cls the closure to fill with test data. */ static void pre_test_products (struct TestProducts_Closure *cls) { /* Instance */ make_instance ("test_inst_products", &cls->instance); /* Products */ make_product ("test_products_pd_0", &cls->products[0]); make_product ("test_products_pd_1", &cls->products[1]); cls->products[1].product.description = "This is a another test product"; cls->products[1].product.unit = "cans"; GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:4.95", &cls->products[1].product.price)); cls->products[1].product.total_stock = 5001; } /** * Handles all teardown after testing. * * @param cls the closure containing memory to be freed. */ static void post_test_products (struct TestProducts_Closure *cls) { free_instance_data (&cls->instance); free_product_data (&cls->products[0]); free_product_data (&cls->products[1]); } /** * Runs the tests for products. * * @param cls the container of the test data. * @return 0 on success, 1 otherwise. */ static int run_test_products (struct TestProducts_Closure *cls) { struct GNUNET_Uuid uuid; struct GNUNET_TIME_Absolute refund_deadline = GNUNET_TIME_absolute_add (GNUNET_TIME_absolute_get (), GNUNET_TIME_UNIT_WEEKS); /* Test that insert without an instance fails */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Insert the instance */ TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test inserting a product */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test that double insert fails */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test lookup of individual products */ TEST_RET_ON_FAIL (test_lookup_product (&cls->instance, &cls->products[0])); /* Make sure it fails correctly for products that don't exist */ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->lookup_product (plugin->cls, cls->instance.instance.id, "nonexistent_product", NULL)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup product failed\n"); return 1; } /* Test product update */ cls->products[0].product.description = "This is a test product that has been updated!"; GNUNET_assert (0 == json_array_append_new ( cls->products[0].product.description_i18n, json_string ( "description in another language"))); cls->products[0].product.unit = "barrels"; GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:7.68", &cls->products[0].product.price)); GNUNET_assert (0 == json_array_append_new (cls->products[0].product.taxes, json_string ("2% sales tax"))); // cls->products[0].product.total_stock = 40; // cls->products[0].product.total_sold = 5; // cls->products[0].product.total_lost = 1; // TEST that these counters cannot be decreased. TEST_RET_ON_FAIL (test_update_product (&cls->instance, &cls->products[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_product (&cls->instance, &cls->products[0])); TEST_RET_ON_FAIL (test_update_product (&cls->instance, &cls->products[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test collective product lookup */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_products (&cls->instance, 2, cls->products)); /* Test locking */ uuid.value[0] = 0x1287346a; if (1 != plugin->lock_product (plugin->cls, cls->instance.instance.id, cls->products[0].id, &uuid, 1, refund_deadline)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lock product failed\n"); return 1; } /* Test product deletion */ TEST_RET_ON_FAIL (test_delete_product (&cls->instance, &cls->products[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double deletion fails */ TEST_RET_ON_FAIL (test_delete_product (&cls->instance, &cls->products[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); TEST_RET_ON_FAIL (test_lookup_products (&cls->instance, 1, cls->products)); /* Test unlocking */ if (1 != plugin->unlock_inventory (plugin->cls, &uuid)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unlock inventory failed\n"); return 1; } TEST_RET_ON_FAIL (test_delete_product (&cls->instance, &cls->products[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_products (&cls->instance, 0, NULL)); return 0; } /** * Takes care of product testing. * * @return 0 on success, 1 otherwise. */ static int test_products () { struct TestProducts_Closure test_cls; pre_test_products (&test_cls); int test_result = run_test_products (&test_cls); post_test_products (&test_cls); return test_result; } /* ********** Orders ********** */ /** * Container for order data */ struct OrderData { /** * The id of the order */ const char *id; /** * The pay deadline for the order */ struct GNUNET_TIME_Absolute pay_deadline; /** * The contract of the order */ json_t *contract; }; /** * Builds an order for testing. * * @param order_id the identifier to use for the order. * @param order the container to fill with data. */ static void make_order (const char *order_id, struct OrderData *order) { struct GNUNET_TIME_Absolute pay_deadline; struct GNUNET_TIME_Absolute refund_deadline; order->id = order_id; order->pay_deadline = GNUNET_TIME_absolute_get_zero_ (); order->contract = json_object (); GNUNET_assert (NULL != order->contract); pay_deadline = GNUNET_TIME_absolute_add (GNUNET_TIME_absolute_get (), GNUNET_TIME_UNIT_DAYS); refund_deadline = GNUNET_TIME_absolute_add (GNUNET_TIME_absolute_get (), GNUNET_TIME_UNIT_WEEKS); GNUNET_TIME_round_abs (&pay_deadline); GNUNET_TIME_round_abs (&refund_deadline); json_object_set (order->contract, "fulfillment_url", json_string ("a")); json_object_set (order->contract, "pay_deadline", GNUNET_JSON_from_time_abs (pay_deadline)); json_object_set (order->contract, "refund_deadline", GNUNET_JSON_from_time_abs (refund_deadline)); } /** * Frees memory associated with an order. * * @param the order to free. */ static void free_order_data (struct OrderData *order) { json_decref (order->contract); } /** * Tests inserting an order into the database. * * @param instance the instance to insert the order for. * @param order the order to insert. * @param expected_result the value we expect the db to return. * @return 0 on success, 1 otherwise. */ static int test_insert_order (const struct InstanceData *instance, const struct OrderData *order, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_order (plugin->cls, instance->instance.id, order->id, order->pay_deadline, order->contract), "Insert order failed\n"); return 0; } /** * Tests looking up an order in the database. * * @param instance the instance to lookup from. * @param order the order that should be looked up. * @return 0 on success, 1 otherwise. */ static int test_lookup_order (const struct InstanceData *instance, const struct OrderData *order) { json_t *lookup_terms = NULL; if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->lookup_order (plugin->cls, instance->instance.id, order->id, &lookup_terms)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup order failed\n"); if (NULL != lookup_terms) json_decref (lookup_terms); return 1; } if (1 != json_equal (order->contract, lookup_terms)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup order failed: incorrect order returned\n"); if (NULL != lookup_terms) json_decref (lookup_terms); return 1; } json_decref (lookup_terms); return 0; } /** * Closure for testing order lookup */ struct TestLookupOrders_Closure { /** * Number of orders to compare to */ unsigned int orders_to_cmp_length; /** * Pointer to (ordered) array of order ids */ const struct OrderData *orders_to_cmp; /** * Pointer to array of bools indicating matches in the correct index */ bool *results_match; /** * Total number of results returned */ unsigned int results_length; }; /** * Called after @e test_lookup_orders. * * @param cls the lookup closure. * @param order_id the identifier of the order found. * @param order_serial the row number of the order found. * @param timestamp when the order was added to the database. */ static void lookup_orders_cb (void *cls, const char *order_id, uint64_t order_serial, struct GNUNET_TIME_Absolute timestamp) { struct TestLookupOrders_Closure *cmp = cls; if (NULL == cmp) return; unsigned int i = cmp->results_length; cmp->results_length += 1; if (cmp->orders_to_cmp_length > i) { /* Compare the orders */ if (0 == strcmp (cmp->orders_to_cmp[i].id, order_id)) cmp->results_match[i] = true; else cmp->results_match[i] = false; } } /** * Tests looking up orders for an instance. * * @param instance the instance. * @param filter the filters applied on the lookup. * @param orders_length the number of orders we expect to find. * @param orders the orders we expect to find. * @return 0 on success, 1 otherwise. */ static int test_lookup_orders (const struct InstanceData *instance, const struct TALER_MERCHANTDB_OrderFilter *filter, unsigned int orders_length, const struct OrderData *orders) { bool results_match[orders_length]; struct TestLookupOrders_Closure cls = { .orders_to_cmp_length = orders_length, .orders_to_cmp = orders, .results_match = results_match, .results_length = 0 }; memset (results_match, 0, sizeof (bool) * orders_length); if (0 > plugin->lookup_orders (plugin->cls, instance->instance.id, filter, &lookup_orders_cb, &cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup orders failed\n"); return 1; } if (orders_length != cls.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup orders failed: incorrect number of results (%d)\n", cls.results_length); return 1; } for (unsigned int i = 0; orders_length > i; ++i) { if (false == cls.results_match[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup orders failed: mismatched data (index %d)\n", i); return 1; } } return 0; } /** * Tests deleting an order from the database. * * @param instance the instance to delete the order from. * @param order the order to delete. * @param expected_result the result we expect to receive. * @return 0 on success, 1 otherwise. */ static int test_delete_order (const struct InstanceData *instance, const struct OrderData *order, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->delete_order (plugin->cls, instance->instance.id, order->id), "Delete order failed\n"); return 0; } /** * Test inserting contract terms for an order. * * @param instance the instance. * @param order the order containing the contract terms. * @param expected_result the result we expect to receive. * @return 0 on success, 1 otherwise. */ static int test_insert_contract_terms (const struct InstanceData *instance, const struct OrderData *order, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_contract_terms (plugin->cls, instance->instance.id, order->id, order->contract), "Insert contract terms failed\n"); return 0; } /** * Tests lookup of contract terms * * @param instance the instance to lookup from. * @param order the order to lookup for. * @return 0 on success, 1 otherwise. */ static int test_lookup_contract_terms (const struct InstanceData *instance, const struct OrderData *order) { json_t *contract = NULL; uint64_t order_serial; if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->lookup_contract_terms (plugin->cls, instance->instance.id, order->id, &contract, &order_serial)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup contract terms failed\n"); GNUNET_assert (NULL == contract); return 1; } if (1 != json_equal (order->contract, contract)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup contract terms failed: mismatched data\n"); json_decref (contract); return 1; } json_decref (contract); return 0; } /** * Tests deleting contract terms for an order. * * @param instance the instance to delete from. * @param order the order whose contract terms we should delete. * @param expected_result the result we expect to receive. * @return 0 on success, 1 otherwise. */ static int test_delete_contract_terms (const struct InstanceData *instance, const struct OrderData *order, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->delete_contract_terms (plugin->cls, instance->instance.id, order->id, GNUNET_TIME_UNIT_MONTHS), "Delete contract terms failed\n"); return 0; } /** * Test marking a contract as paid in the database. * * @param instance the instance to use. * @param order the order whose contract to use. * @param expected_result the result we expect to receive. * @return 0 on success, 1 otherwise. */ static int test_mark_contract_paid (const struct InstanceData *instance, const struct OrderData *order, enum GNUNET_DB_QueryStatus expected_result) { struct GNUNET_HashCode h_contract_terms; GNUNET_assert (GNUNET_OK == TALER_JSON_hash (order->contract, &h_contract_terms)); TEST_COND_RET_ON_FAIL (expected_result == plugin->mark_contract_paid (plugin->cls, instance->instance.id, &h_contract_terms, "test_orders_session"), "Mark contract paid failed\n"); return 0; } /** * Tests looking up the status of an order. * * @param instance the instance to lookup from. * @param order the order to lookup. * @param expected_paid whether the order was paid or not. * @return 0 on success, 1 otherwise. */ static int test_lookup_order_status (const struct InstanceData *instance, const struct OrderData *order, bool expected_paid) { struct GNUNET_HashCode h_contract_terms_expected; struct GNUNET_HashCode h_contract_terms; bool order_paid = false; if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->lookup_order_status (plugin->cls, instance->instance.id, order->id, &h_contract_terms, &order_paid)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup order status failed\n"); return 1; } GNUNET_assert (GNUNET_OK == TALER_JSON_hash (order->contract, &h_contract_terms_expected)); if ((expected_paid != order_paid) || (0 != GNUNET_memcmp (&h_contract_terms, &h_contract_terms_expected))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup order status/deposit failed: mismatched data\n"); return 1; } return 0; } /** * Test looking up an order by its fulfillment. * * @param instance the instance to lookup from. * @param order the order to lookup. * @param the session id associated with the payment. * @return 0 on success, 1 otherwise. */ static int test_lookup_order_by_fulfillment (const struct InstanceData *instance, const struct OrderData *order, const char *session_id) { char *order_id; const char *fulfillment_url = json_string_value (json_object_get (order->contract, "fulfillment_url")); GNUNET_assert (NULL != fulfillment_url); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->lookup_order_by_fulfillment (plugin->cls, instance->instance.id, fulfillment_url, session_id, &order_id)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup order by fulfillment failed\n"); GNUNET_free (order_id); return 1; } if (0 != strcmp (order->id, order_id)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup order by fulfillment failed\n"); GNUNET_free (order_id); return 1; } GNUNET_free (order_id); return 0; } /** * Test looking up the status of an order. * * @param order_id the row of the order in the database. * @param session_id the session id associated with the payment. * @param expected_paid whether the order was paid or not. * @param expected_wired whether the order was wired or not. * @return 0 on success, 1 otherwise. */ static int test_lookup_payment_status (uint64_t order_id, const char *session_id, bool expected_paid, bool expected_wired) { bool paid; bool wired; TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->lookup_payment_status (plugin->cls, order_id, session_id, &paid, &wired), "Lookup payment status failed\n"); if ((expected_paid != paid) || (expected_wired != wired)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup payment status failed\n"); return 1; } return 0; } /** * Test marking an order as being wired. * * @param order_id the row of the order in the database. * @param expected_result the result we expect the plugin to return. * @return 0 on success, 1 otherwise. */ static int test_mark_order_wired (uint64_t order_id, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->mark_order_wired (plugin->cls, order_id), "Mark order wired failed\n"); return 0; } /** * Closure for order tests. */ struct TestOrders_Closure { /** * The instance to use for the order tests. */ struct InstanceData instance; /** * A product to use for the order tests. */ struct ProductData product; /** * The array of orders */ struct OrderData orders[2]; }; /** * Initializes order test data. * * @param cls the order test closure. */ static void pre_test_orders (struct TestOrders_Closure *cls) { /* Instance */ make_instance ("test_inst_orders", &cls->instance); /* Product */ make_product ("test_orders_pd_0", &cls->product); /* Orders */ make_order ("test_orders_od_0", &cls->orders[0]); make_order ("test_orders_od_1", &cls->orders[1]); GNUNET_assert (0 == json_object_set_new (cls->orders[1].contract, "other_field", json_string ("Second contract"))); } /** * Frees memory after order tests. * * @param cls the order test closure. */ static void post_test_orders (struct TestOrders_Closure *cls) { free_instance_data (&cls->instance); free_product_data (&cls->product); free_order_data (&cls->orders[0]); free_order_data (&cls->orders[1]); } /** * Run the tests for orders. * * @param cls the order test closure. * @return 0 on success, 1 on failure. */ static int run_test_orders (struct TestOrders_Closure *cls) { struct TALER_MERCHANTDB_OrderFilter filter = { .paid = TALER_EXCHANGE_YNA_ALL, .refunded = TALER_EXCHANGE_YNA_ALL, .wired = TALER_EXCHANGE_YNA_ALL, .date = GNUNET_TIME_absolute_get_zero_ (), .start_row = 0, .delta = 8 }; /* Insert the instance */ TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test inserting an order */ TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double insert fails */ TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test lookup order */ TEST_RET_ON_FAIL (test_lookup_order (&cls->instance, &cls->orders[0])); /* Make sure it fails correctly for nonexistent orders */ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->lookup_order (plugin->cls, cls->instance.instance.id, cls->orders[1].id, NULL)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup order failed\n"); return 1; } /* Test lookups on multiple orders */ TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, &filter, 2, cls->orders)); /* Test inserting contract terms */ TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double insert fails */ TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test order lock */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->product, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); if (1 != plugin->insert_order_lock (plugin->cls, cls->instance.instance.id, cls->orders[0].id, cls->product.id, 5)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Insert order lock failed\n"); return 1; } /* Test lookup contract terms */ TEST_RET_ON_FAIL (test_lookup_contract_terms (&cls->instance, &cls->orders[0])); /* Test lookup fails for nonexistent contract terms */ { json_t *lookup_contract = NULL; uint64_t lookup_order_serial; if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->lookup_contract_terms (plugin->cls, cls->instance.instance.id, cls->orders[1].id, &lookup_contract, &lookup_order_serial)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup contract terms failed\n"); GNUNET_assert (NULL == lookup_contract); return 1; } } /* Test lookup order status */ TEST_RET_ON_FAIL (test_lookup_order_status (&cls->instance, &cls->orders[0], false)); /* Test lookup payment status */ TEST_RET_ON_FAIL (test_lookup_payment_status (1, NULL, false, false)); /* Test lookup order status fails for nonexistent order */ { struct GNUNET_HashCode h_contract_terms; bool order_paid; if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->lookup_order_status (plugin->cls, cls->instance.instance.id, cls->orders[1].id, &h_contract_terms, &order_paid)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup order status failed\n"); return 1; } } /* Test marking contracts as paid */ TEST_RET_ON_FAIL (test_mark_contract_paid (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_payment_status (1, NULL, true, false)); TEST_RET_ON_FAIL (test_lookup_payment_status (1, "test_orders_session", true, false)); /* Test lookup order by fulfillment */ TEST_RET_ON_FAIL (test_lookup_order_by_fulfillment (&cls->instance, &cls->orders[0], "test_orders_session")); /* Test mark as paid fails for nonexistent order */ TEST_RET_ON_FAIL (test_mark_contract_paid (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); TEST_RET_ON_FAIL (test_lookup_order_status (&cls->instance, &cls->orders[0], true)); filter.paid = TALER_EXCHANGE_YNA_YES; TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, &filter, 1, cls->orders)); /* Test marking orders as wired */ TEST_RET_ON_FAIL (test_mark_order_wired (1, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_payment_status (1, NULL, true, true)); TEST_RET_ON_FAIL (test_mark_order_wired (1007, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test deleting contract terms */ TEST_RET_ON_FAIL (test_delete_contract_terms (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test we can't delete something that doesn't exist */ TEST_RET_ON_FAIL (test_delete_contract_terms (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test delete order */ TEST_RET_ON_FAIL (test_delete_order (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, &filter, 0, NULL)); /* Test we can't delete something that doesn't exist */ TEST_RET_ON_FAIL (test_delete_order (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); return 0; } /** * Does all tasks for testing orders. * * @return 0 when successful, 1 otherwise. */ static int test_orders () { struct TestOrders_Closure test_cls; pre_test_orders (&test_cls); int test_result = run_test_orders (&test_cls); post_test_orders (&test_cls); return test_result; } /* Deposits */ struct ExchangeSignkeyData { struct TALER_MasterPrivateKeyP master_priv; struct TALER_MasterPublicKeyP master_pub; struct TALER_MasterSignatureP master_sig; struct TALER_ExchangePrivateKeyP exchange_priv; struct TALER_ExchangePublicKeyP exchange_pub; struct GNUNET_TIME_Absolute start_date; struct GNUNET_TIME_Absolute expire_date; struct GNUNET_TIME_Absolute end_date; }; static void make_exchange_signkey (struct ExchangeSignkeyData *signkey) { struct TALER_ExchangeSigningKeyValidityPS exch_sign = { .purpose = { .size = htonl (sizeof (struct TALER_ExchangeSigningKeyValidityPS)), .purpose = htonl (TALER_SIGNATURE_MASTER_SIGNING_KEY_VALIDITY) } }; GNUNET_CRYPTO_eddsa_key_create (&signkey->exchange_priv.eddsa_priv); GNUNET_CRYPTO_eddsa_key_get_public (&signkey->exchange_priv.eddsa_priv, &signkey->exchange_pub.eddsa_pub); GNUNET_CRYPTO_eddsa_key_create (&signkey->master_priv.eddsa_priv); GNUNET_CRYPTO_eddsa_key_get_public (&signkey->master_priv.eddsa_priv, &signkey->master_pub.eddsa_pub); GNUNET_CRYPTO_eddsa_sign (&signkey->master_priv.eddsa_priv, &exch_sign, &signkey->master_sig.eddsa_signature); signkey->start_date = GNUNET_TIME_absolute_get (); signkey->expire_date = GNUNET_TIME_absolute_get (); signkey->end_date = GNUNET_TIME_absolute_get (); } /** * A container for deposit data. */ struct DepositData { struct GNUNET_TIME_Absolute timestamp; struct GNUNET_HashCode h_contract_terms; struct TALER_CoinSpendPublicKeyP coin_pub; const char *exchange_url; struct TALER_Amount amount_with_fee; struct TALER_Amount deposit_fee; struct TALER_Amount refund_fee; struct TALER_Amount wire_fee; struct GNUNET_HashCode h_wire; struct TALER_ExchangeSignatureP exchange_sig; }; static void make_deposit (const struct InstanceData *instance, const struct TALER_MERCHANTDB_AccountDetails *account, const struct OrderData *order, const struct ExchangeSignkeyData *signkey, struct DepositData *deposit) { struct TALER_CoinSpendPrivateKeyP coin_priv; struct TALER_DepositRequestPS deposit_sign = { .purpose = { .size = htonl (sizeof (struct TALER_DepositRequestPS)), .purpose = htonl (TALER_SIGNATURE_WALLET_COIN_DEPOSIT) } }; deposit->timestamp = GNUNET_TIME_absolute_get (); GNUNET_assert (GNUNET_OK == TALER_JSON_hash (order->contract, &deposit->h_contract_terms)); GNUNET_CRYPTO_eddsa_key_create (&coin_priv.eddsa_priv); GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv, &deposit->coin_pub.eddsa_pub); deposit->exchange_url = "test-exchange"; GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:50.00", &deposit->amount_with_fee)); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:1.00", &deposit->deposit_fee)); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:1.50", &deposit->refund_fee)); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:2.00", &deposit->wire_fee)); deposit->h_wire = account->h_wire; deposit_sign.h_contract_terms = deposit->h_contract_terms; deposit_sign.h_wire = deposit->h_wire; deposit_sign.wallet_timestamp = GNUNET_TIME_absolute_hton ( GNUNET_TIME_absolute_get ()); deposit_sign.refund_deadline = GNUNET_TIME_absolute_hton ( GNUNET_TIME_absolute_get ()); TALER_amount_hton (&deposit_sign.amount_with_fee, &deposit->amount_with_fee); TALER_amount_hton (&deposit_sign.deposit_fee, &deposit->deposit_fee); deposit_sign.merchant = instance->merchant_pub; deposit_sign.coin_pub = deposit->coin_pub; GNUNET_CRYPTO_eddsa_sign (&signkey->exchange_priv.eddsa_priv, &deposit_sign, &deposit->exchange_sig.eddsa_signature); } /** * Closure for testing deposit lookup */ struct TestLookupDeposits_Closure { /** * Number of deposits to compare to */ unsigned int deposits_to_cmp_length; /** * Pointer to array of deposit data */ const struct DepositData *deposits_to_cmp; /** * Pointer to array of number of matches per deposit */ unsigned int *results_matching; /** * Total number of results returned */ unsigned int results_length; }; static void lookup_deposits_cb (void *cls, const char *exchange_url, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_Amount *refund_fee, const struct TALER_Amount *wire_fee) { struct TestLookupDeposits_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; cmp->deposits_to_cmp_length > i; ++i) { if ((GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].amount_with_fee, amount_with_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].amount_with_fee, amount_with_fee)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].deposit_fee, deposit_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].deposit_fee, deposit_fee)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].refund_fee, refund_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].refund_fee, refund_fee)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].wire_fee, wire_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].wire_fee, wire_fee))) { cmp->results_matching[i] += 1; } } } static void lookup_deposits_contract_coin_cb (void *cls, const char *exchange_url, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_Amount *refund_fee, const struct TALER_Amount *wire_fee, const struct GNUNET_HashCode *h_wire, struct GNUNET_TIME_Absolute deposit_timestamp, struct GNUNET_TIME_Absolute refund_deadline, const struct TALER_ExchangeSignatureP *exchange_sig, const struct TALER_ExchangePublicKeyP *exchange_pub) { struct TestLookupDeposits_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; cmp->deposits_to_cmp_length > i; ++i) { if ((cmp->deposits_to_cmp[i].timestamp.abs_value_us == deposit_timestamp.abs_value_us) && (0 == strcmp (cmp->deposits_to_cmp[i].exchange_url, exchange_url)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].amount_with_fee, amount_with_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].amount_with_fee, amount_with_fee)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].deposit_fee, deposit_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].deposit_fee, deposit_fee)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].refund_fee, refund_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].refund_fee, refund_fee)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].wire_fee, wire_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].wire_fee, wire_fee)) && (0 == GNUNET_memcmp (&cmp->deposits_to_cmp[i].h_wire, h_wire)) && (0 == GNUNET_memcmp (&cmp->deposits_to_cmp[i].exchange_sig, exchange_sig))) { cmp->results_matching[i] += 1; } } } static int test_insert_exchange_signkey (const struct ExchangeSignkeyData *signkey, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_exchange_signkey (plugin->cls, &signkey->master_pub, &signkey->exchange_pub, signkey->start_date, signkey->expire_date, signkey->end_date, &signkey->master_sig), "Insert exchange signkey failed\n"); return 0; } static int test_insert_deposit (const struct InstanceData *instance, const struct ExchangeSignkeyData *signkey, const struct DepositData *deposit, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_deposit (plugin->cls, instance->instance.id, deposit->timestamp, &deposit->h_contract_terms, &deposit->coin_pub, deposit->exchange_url, &deposit->amount_with_fee, &deposit->deposit_fee, &deposit->refund_fee, &deposit->wire_fee, &deposit->h_wire, &deposit->exchange_sig, &signkey->exchange_pub), "Insert deposit failed\n"); return 0; } static int test_lookup_deposits (const struct InstanceData *instance, const struct GNUNET_HashCode *h_contract_terms, unsigned int deposits_length, const struct DepositData *deposits) { unsigned int results_matching[deposits_length]; struct TestLookupDeposits_Closure cmp = { .deposits_to_cmp_length = deposits_length, .deposits_to_cmp = deposits, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * deposits_length); TEST_COND_RET_ON_FAIL (deposits_length == plugin->lookup_deposits (plugin->cls, instance->instance.id, h_contract_terms, &lookup_deposits_cb, &cmp), "Lookup deposits failed\n"); if (deposits_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup deposits failed: incorrect number of results returned\n"); return 1; } for (unsigned int i = 0; deposits_length > i; ++i) { if (cmp.results_matching[i] != 1) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup deposits failed: mismatched data\n"); return 1; } } return 0; } static int test_lookup_deposits_contract_and_coin (const struct InstanceData *instance, const struct GNUNET_HashCode *h_contract, const struct TALER_CoinSpendPublicKeyP *coin_pub, unsigned int deposits_length, const struct DepositData *deposits) { unsigned int results_matching[deposits_length]; struct TestLookupDeposits_Closure cmp = { .deposits_to_cmp_length = deposits_length, .deposits_to_cmp = deposits, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * deposits_length); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->lookup_deposits_by_contract_and_coin ( plugin->cls, instance ->instance.id, h_contract, coin_pub, & lookup_deposits_contract_coin_cb, &cmp), "Lookup deposits by contract and coin failed\n"); if (deposits_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup deposits failed: incorrect number of results returned\n"); return 1; } for (unsigned int i = 0; deposits_length > i; ++i) { if (cmp.results_matching[i] != 1) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup deposits failed: mismatched data\n"); return 1; } } return 0; } static void lookup_deposits_order_cb (void *cls, uint64_t deposit_serial, const char *exchange_url, const struct GNUNET_HashCode *h_wire, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub) { struct TestLookupDeposits_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; i < cmp->deposits_to_cmp_length; ++i) { if ((0 == strcmp (cmp->deposits_to_cmp[i].exchange_url, exchange_url)) && (0 == GNUNET_memcmp (&cmp->deposits_to_cmp[i].h_wire, h_wire)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].amount_with_fee, amount_with_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].amount_with_fee, amount_with_fee)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->deposits_to_cmp[i].deposit_fee, deposit_fee)) && (0 == TALER_amount_cmp (&cmp->deposits_to_cmp[i].deposit_fee, deposit_fee)) && (0 == GNUNET_memcmp (&cmp->deposits_to_cmp[i].coin_pub, coin_pub))) cmp->results_matching[i] += 1; } } static int test_lookup_deposits_by_order (uint64_t order_serial, unsigned int deposits_length, const struct DepositData *deposits) { unsigned int results_matching[deposits_length]; struct TestLookupDeposits_Closure cmp = { .deposits_to_cmp_length = deposits_length, .deposits_to_cmp = deposits, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * deposits_length); if (deposits_length != plugin->lookup_deposits_by_order (plugin->cls, order_serial, & lookup_deposits_order_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup deposits by order failed\n"); return 1; } if (deposits_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup deposits by order failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; i < deposits_length; ++i) { if (1 != cmp.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup deposits by order failed: mismatched data\n"); return 1; } } return 0; } /** * Closure for deposit tests. */ struct TestDeposits_Closure { /** * The instance settings */ struct InstanceData instance; /** * The merchant account */ struct TALER_MERCHANTDB_AccountDetails account; /** * The exchange signing key */ struct ExchangeSignkeyData signkey; /** * The order data */ struct OrderData order; /** * The array of deposits */ struct DepositData deposits[2]; }; static void pre_test_deposits (struct TestDeposits_Closure *cls) { /* Instance */ make_instance ("test_inst_deposits", &cls->instance); /* Account */ make_account (&cls->account); /* Signing key */ make_exchange_signkey (&cls->signkey); /* Order */ make_order ("test_deposits_od_1", &cls->order); /* Deposit */ make_deposit (&cls->instance, &cls->account, &cls->order, &cls->signkey, &cls->deposits[0]); make_deposit (&cls->instance, &cls->account, &cls->order, &cls->signkey, &cls->deposits[1]); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:29.00", &cls->deposits[1].amount_with_fee)); } static void post_test_deposits (struct TestDeposits_Closure *cls) { free_instance_data (&cls->instance); json_decref (cls->order.contract); } static int run_test_deposits (struct TestDeposits_Closure *cls) { /* Insert the instance */ TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert an account */ TEST_RET_ON_FAIL (test_insert_account (&cls->instance, &cls->account, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert a signing key */ TEST_RET_ON_FAIL (test_insert_exchange_signkey (&cls->signkey, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert an order */ TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->order, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert contract terms */ TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->order, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test inserting a deposit */ TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposits[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double inserts fail */ TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposits[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test lookup deposits */ TEST_RET_ON_FAIL (test_lookup_deposits (&cls->instance, &cls->deposits[0].h_contract_terms, 1, cls->deposits)); /* Test lookup deposits by contract and coins */ TEST_RET_ON_FAIL (test_lookup_deposits_contract_and_coin ( &cls->instance, &cls->deposits[0].h_contract_terms, &cls->deposits[0].coin_pub, 1, cls->deposits)); /* Test multiple deposits */ TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposits[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_deposits (&cls->instance, &cls->deposits[0].h_contract_terms, 2, cls->deposits)); /* Test lookup deposits by order */ TEST_RET_ON_FAIL (test_lookup_deposits_by_order (4, 2, cls->deposits)); return 0; } static int test_deposits (void *cls) { struct TestDeposits_Closure test_cls; pre_test_deposits (&test_cls); int test_result = run_test_deposits (&test_cls); post_test_deposits (&test_cls); return test_result; } /* Transfers */ struct WireFeeData { const char *wire_method; struct GNUNET_HashCode h_wire_method; struct TALER_Amount wire_fee; struct TALER_Amount closing_fee; struct GNUNET_TIME_Absolute wire_fee_start; struct GNUNET_TIME_Absolute wire_fee_end; struct TALER_MasterSignatureP fee_sig; }; static void make_wire_fee (const struct ExchangeSignkeyData *signkey, struct WireFeeData *wire_fee) { struct TALER_MasterWireFeePS fee_sign = { .purpose = { .size = htonl (sizeof (struct TALER_MasterWireFeePS)), .purpose = htonl (TALER_SIGNATURE_MASTER_WIRE_FEES) } }; wire_fee->wire_method = "wire-method"; GNUNET_CRYPTO_hash (wire_fee->wire_method, strlen (wire_fee->wire_method) + 1, &wire_fee->h_wire_method); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:0.49", &wire_fee->wire_fee)); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:0.49", &wire_fee->closing_fee)); wire_fee->wire_fee_start = GNUNET_TIME_absolute_get (); wire_fee->wire_fee_end = GNUNET_TIME_absolute_add (wire_fee->wire_fee_start, GNUNET_TIME_UNIT_MONTHS); fee_sign.h_wire_method = wire_fee->h_wire_method; TALER_amount_hton (&fee_sign.wire_fee, &wire_fee->wire_fee); TALER_amount_hton (&fee_sign.closing_fee, &wire_fee->closing_fee); fee_sign.start_date = GNUNET_TIME_absolute_hton (wire_fee->wire_fee_start); fee_sign.end_date = GNUNET_TIME_absolute_hton (wire_fee->wire_fee_end); GNUNET_CRYPTO_eddsa_sign (&signkey->master_priv.eddsa_priv, &fee_sign, &wire_fee->fee_sig.eddsa_signature); } struct TransferWithDetails { struct TALER_WireTransferIdentifierRawP wtid; struct TALER_EXCHANGE_TransferData details; }; static int test_lookup_transfer (const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, const struct TALER_Amount *total_expected, const struct TALER_Amount *fee_expected, const struct GNUNET_TIME_Absolute *time_expected, bool verified_expected) { struct TALER_Amount total; struct TALER_Amount fee; struct GNUNET_TIME_Absolute time; bool verified; if (1 != plugin->lookup_transfer (plugin->cls, exchange_url, wtid, &total, &fee, &time, &verified)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer failed\n"); return 1; } if ((GNUNET_OK != TALER_amount_cmp_currency (total_expected, &total)) || (0 != TALER_amount_cmp (total_expected, &total)) || (GNUNET_OK != TALER_amount_cmp_currency (fee_expected, &fee)) || (0 != TALER_amount_cmp (fee_expected, &fee)) || (time_expected->abs_value_us != time.abs_value_us) || (verified_expected != verified)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer failed: mismatched data\n"); return 1; } return 0; } struct TestLookupTransferSummary_Closure { const char *order_id; const struct TALER_Amount *deposit_value; const struct TALER_Amount *deposit_fee; int result; }; static void lookup_transfer_summary_cb (void *cls, const char *order_id, const struct TALER_Amount *deposit_value, const struct TALER_Amount *deposit_fee) { struct TestLookupTransferSummary_Closure *cmp = cls; if (NULL == cmp) return; if ((0 == strcmp (cmp->order_id, order_id)) && (GNUNET_OK == TALER_amount_cmp_currency (cmp->deposit_value, deposit_value)) && (0 == TALER_amount_cmp (cmp->deposit_value, deposit_value)) && (GNUNET_OK == TALER_amount_cmp_currency (cmp->deposit_fee, deposit_fee)) && (0 == TALER_amount_cmp (cmp->deposit_fee, deposit_fee))) cmp->result = 0; else cmp->result = 1; } static int test_lookup_transfer_summary (const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, const char *expected_order_id, const struct TALER_Amount *expected_deposit_value, const struct TALER_Amount *expected_deposit_fee) { struct TestLookupTransferSummary_Closure cmp = { .order_id = expected_order_id, .deposit_value = expected_deposit_value, .deposit_fee = expected_deposit_fee, .result = 0 }; if (1 != plugin->lookup_transfer_summary (plugin->cls, exchange_url, wtid, &lookup_transfer_summary_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer summary failed\n"); return 1; } if (0 != cmp.result) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer summary failed: mismatched data\n"); return 1; } return 0; } struct TestLookupTransferDetails_Closure { unsigned int details_to_cmp_length; const struct TALER_TrackTransferDetails *details_to_cmp; unsigned int *results_matching; unsigned int results_length; }; static void lookup_transfer_details_cb (void *cls, unsigned int current_offset, const struct TALER_TrackTransferDetails *details) { struct TestLookupTransferDetails_Closure *cmp = cls; if (NULL == cmp) return; for (unsigned int i = 0; cmp->details_to_cmp_length > i; ++i) { if ((0 == GNUNET_memcmp (&cmp->details_to_cmp[i].h_contract_terms, &details->h_contract_terms)) && (0 == GNUNET_memcmp (&cmp->details_to_cmp[i].coin_pub, &details->coin_pub)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->details_to_cmp[i].coin_value, &details->coin_value)) && (0 == TALER_amount_cmp (&cmp->details_to_cmp[i].coin_value, &details->coin_value)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->details_to_cmp[i].coin_fee, &details->coin_fee)) && (0 == TALER_amount_cmp (&cmp->details_to_cmp[i].coin_fee, &details->coin_fee))) { cmp->results_matching[i] += 1; } } cmp->results_length += 1; } static int test_lookup_transfer_details (const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, unsigned int details_length, const struct TALER_TrackTransferDetails *details) { unsigned int results_matching[details_length]; struct TestLookupTransferDetails_Closure cmp = { .details_to_cmp_length = details_length, .details_to_cmp = details, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * details_length); if (1 != plugin->lookup_transfer_details (plugin->cls, exchange_url, wtid, &lookup_transfer_details_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer details failed\n"); return 1; } if (details_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer details failed: incorrect number of results (%d)\n", cmp.results_length); return 1; } for (unsigned int i = 0; details_length > i; ++i) { if (1 != cmp.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer details failed: mismatched data\n"); return 1; } } return 0; } struct TransferContainer { struct TALER_WireTransferIdentifierRawP wtid; const char *exchange_url; struct GNUNET_TIME_Absolute execution_time; struct TALER_Amount deposit_value; struct TALER_Amount deposit_fee; bool confirmed; }; struct TestLookupTransferDetailsByOrder_Closure { unsigned int transfers_to_cmp_length; const struct TransferContainer *transfers_to_cmp; unsigned int *results_matching; unsigned int results_length; }; static void lookup_transfer_details_order_cb (void *cls, const struct TALER_WireTransferIdentifierRawP *wtid, const char *exchange_url, struct GNUNET_TIME_Absolute execution_time, const struct TALER_Amount *deposit_value, const struct TALER_Amount *deposit_fee, bool transfer_confirmed) { struct TestLookupTransferDetailsByOrder_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; i < cmp->transfers_to_cmp_length; ++i) { /* Right now lookup_transfer_details_by_order leaves execution_time uninitialized and transfer_confirmed always false. */ if ((0 == GNUNET_memcmp (&cmp->transfers_to_cmp[i].wtid, wtid)) && (0 == strcmp (cmp->transfers_to_cmp[i].exchange_url, exchange_url)) && /*(cmp->transfers_to_cmp[i].execution_time.abs_value_us == execution_time.abs_value_us) &&*/ (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->transfers_to_cmp[i].deposit_value, deposit_value)) && (0 == TALER_amount_cmp (&cmp->transfers_to_cmp[i].deposit_value, deposit_value)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->transfers_to_cmp[i].deposit_fee, deposit_fee)) && (0 == TALER_amount_cmp (&cmp->transfers_to_cmp[i].deposit_fee, deposit_fee)) /* && (cmp->transfers_to_cmp[i].confirmed == transfer_confirmed)*/) cmp->results_matching[i] += 1; } } static int test_lookup_transfer_details_by_order (uint64_t order_serial, unsigned int transfers_length, const struct TransferContainer *transfers) { unsigned int results_matching[transfers_length]; struct TestLookupTransferDetailsByOrder_Closure cmp = { .transfers_to_cmp_length = transfers_length, .transfers_to_cmp = transfers, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * transfers_length); if (transfers_length != plugin->lookup_transfer_details_by_order (plugin->cls, order_serial, & lookup_transfer_details_order_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer details by order failed\n"); return 1; } if (transfers_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer details by order failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; i < transfers_length; ++i) { if (1 != cmp.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer details by order failed: mismatched data\n"); return 1; } } return 0; } static int test_insert_wire_fee (const struct ExchangeSignkeyData *signkey, const struct WireFeeData *wire_fee, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->store_wire_fee_by_exchange (plugin->cls, &signkey-> master_pub, &wire_fee-> h_wire_method, &wire_fee->wire_fee, &wire_fee-> closing_fee, wire_fee-> wire_fee_start, wire_fee-> wire_fee_end, &wire_fee->fee_sig), "Store wire fee by exchange failed\n"); return 0; } static int test_lookup_wire_fee (const struct ExchangeSignkeyData *signkey, const struct WireFeeData *wire_fee_data) { struct TALER_Amount wire_fee; struct TALER_Amount closing_fee; struct GNUNET_TIME_Absolute start_date; struct GNUNET_TIME_Absolute end_date; struct TALER_MasterSignatureP master_sig; if (1 != plugin->lookup_wire_fee (plugin->cls, &signkey->master_pub, wire_fee_data->wire_method, GNUNET_TIME_absolute_get (), &wire_fee, &closing_fee, &start_date, &end_date, &master_sig)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup wire fee failed\n"); return 1; } if ((GNUNET_OK != TALER_amount_cmp_currency (&wire_fee_data->wire_fee, &wire_fee)) || (0 != TALER_amount_cmp (&wire_fee_data->wire_fee, &wire_fee)) || (GNUNET_OK != TALER_amount_cmp_currency (&wire_fee_data->closing_fee, &closing_fee)) || (0 != TALER_amount_cmp (&wire_fee_data->closing_fee, &closing_fee)) || (wire_fee_data->wire_fee_start.abs_value_us != start_date.abs_value_us) || (wire_fee_data->wire_fee_end.abs_value_us != end_date.abs_value_us) || (0 != GNUNET_memcmp (&wire_fee_data->fee_sig, &master_sig))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup wire fee failed: mismatched data\n"); return 1; } return 0; } struct TestLookupTransfers_Closure { unsigned int transfers_to_cmp_length; const struct TransferWithDetails *transfers_to_cmp; unsigned int *results_matching; unsigned int results_length; }; static void lookup_transfers_cb (void *cls, const struct TALER_Amount *credit_amount, const struct TALER_WireTransferIdentifierRawP *wtid, const char *payto_uri, const char *exchange_url, uint64_t transfer_serial_id, struct GNUNET_TIME_Absolute execution_time, bool verified, bool confirmed) { struct TestLookupTransfers_Closure *cmp = cls; if (NULL == cmp) return; for (unsigned int i = 0; cmp->transfers_to_cmp_length > i; ++i) { if ((GNUNET_OK == TALER_amount_cmp_currency ( &cmp->transfers_to_cmp[i].details.total_amount, credit_amount)) && (0 == TALER_amount_cmp (&cmp->transfers_to_cmp[i].details.total_amount, credit_amount)) && (cmp->transfers_to_cmp[i].details.execution_time.abs_value_us == execution_time.abs_value_us)) { cmp->results_matching[i] += 1; } } cmp->results_length += 1; } static int test_lookup_transfers (const char *instance_id, const char *payto_uri, struct GNUNET_TIME_Absolute before, struct GNUNET_TIME_Absolute after, int64_t limit, uint64_t offset, enum TALER_EXCHANGE_YesNoAll filter_verified, unsigned int transfers_length, const struct TransferWithDetails *transfers) { unsigned int results_matching[transfers_length]; struct TestLookupTransfers_Closure cmp = { .transfers_to_cmp_length = transfers_length, .transfers_to_cmp = transfers, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * transfers_length); if (1 != plugin->lookup_transfers (plugin->cls, instance_id, payto_uri, before, after, limit, offset, filter_verified, &lookup_transfers_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfers failed\n"); return 1; } if (transfers_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfers failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; transfers_length > i; ++i) { if (1 != cmp.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfers failed: mismatched data\n"); return 1; } } return 0; } struct TestTransfers_Closure { struct InstanceData instance; /** * The account */ struct TALER_MERCHANTDB_AccountDetails account; /** * The exchange signing key */ struct ExchangeSignkeyData signkey; /** * The order data */ struct OrderData order; /** * The deposit data */ struct DepositData deposit; /** * Wire fee data */ struct WireFeeData wire_fee; }; static void pre_test_transfers (struct TestTransfers_Closure *cls) { /* Instance */ make_instance ("test_inst_transfers", &cls->instance); /* Account */ make_account (&cls->account); /* Order */ make_order ("test_transfers_od_1", &cls->order); /* Signing key */ make_exchange_signkey (&cls->signkey); /* Deposit */ make_deposit (&cls->instance, &cls->account, &cls->order, &cls->signkey, &cls->deposit); /* Wire fee */ make_wire_fee (&cls->signkey, &cls->wire_fee); } static void post_test_transfers (struct TestTransfers_Closure *cls) { free_instance_data (&cls->instance); free_order_data (&cls->order); } static int run_test_transfers (struct TestTransfers_Closure *cls) { struct TALER_WireTransferIdentifierRawP wtid = { .raw = {0} }; struct TALER_Amount amount; struct TALER_Amount total_with_fee; struct TALER_EXCHANGE_TransferData transfer_data = { .exchange_pub = cls->signkey.exchange_pub, .execution_time = GNUNET_TIME_absolute_get (), }; struct TALER_TrackTransferDetails transfer_detail = { .h_contract_terms = cls->deposit.h_contract_terms, .coin_pub = cls->deposit.coin_pub, .coin_value = cls->deposit.amount_with_fee, .coin_fee = cls->deposit.deposit_fee }; struct TransferWithDetails full_transfer_data = { .wtid = wtid }; /* Test lookup wire fee fails when it isn't in the db */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->lookup_wire_fee (plugin->cls, &cls->signkey.master_pub, cls->wire_fee.wire_method, GNUNET_TIME_absolute_get (), NULL, NULL, NULL, NULL, NULL), "Lookup wire fee failed\n"); /* Test store wire fee by exchange */ TEST_RET_ON_FAIL (test_insert_wire_fee (&cls->signkey, &cls->wire_fee, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double insertion fails */ TEST_RET_ON_FAIL (test_insert_wire_fee (&cls->signkey, &cls->wire_fee, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test lookup wire fee by exchange */ TEST_RET_ON_FAIL (test_lookup_wire_fee (&cls->signkey, &cls->wire_fee)); /* Insert the instance */ TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert the account */ TEST_RET_ON_FAIL (test_insert_account (&cls->instance, &cls->account, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert a signing key */ TEST_RET_ON_FAIL (test_insert_exchange_signkey (&cls->signkey, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert an order */ TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->order, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert contract terms */ TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->order, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert the deposit */ TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposit, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); transfer_data.details_length = 1; transfer_data.details = &transfer_detail; /* Insert the transfer */ GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:50.00", &amount)); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->insert_transfer (plugin->cls, cls->instance.instance.id, cls->deposit.exchange_url, &wtid, &amount, cls->account.payto_uri, true), "Insert transfer failed\n"); /* Test that double insert fails */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->insert_transfer (plugin->cls, cls->instance.instance.id, cls->deposit.exchange_url, &wtid, &amount, cls->account.payto_uri, true), "Insert transfer failed\n"); /* Test transfer details */ GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:50.00", &transfer_data.total_amount)); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:0.49", &transfer_data.wire_fee)); TEST_RET_ON_FAIL (test_lookup_payment_status (5, NULL, false, false)); /* Test insert deposit to transfer */ { const struct TALER_EXCHANGE_DepositData deposit_data = { .exchange_pub = cls->signkey.exchange_pub, .exchange_sig = cls->deposit.exchange_sig, .wtid = wtid, .execution_time = transfer_data.execution_time, .coin_contribution = transfer_data.total_amount }; TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->insert_deposit_to_transfer (plugin->cls, 4, &deposit_data), "Insert deposit to transfer failed\n"); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->insert_deposit_to_transfer (plugin->cls, 4, &deposit_data), "Insert deposit to transfer failed\n"); } TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->insert_transfer_details (plugin->cls, cls->instance.instance .id, cls->deposit. exchange_url, cls->account.payto_uri, &wtid, &transfer_data), "Insert transfer details failed\n"); TEST_RET_ON_FAIL (test_lookup_payment_status (5, NULL, false, true)); /* Test lookup transfer */ GNUNET_assert (0 <= TALER_amount_add (&total_with_fee, &transfer_data.total_amount, &transfer_data.wire_fee)); TEST_RET_ON_FAIL (test_lookup_transfer (cls->deposit.exchange_url, &wtid, &total_with_fee, &transfer_data.wire_fee, &transfer_data.execution_time, false)); /* Test set status to verified */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->set_transfer_status_to_verified (plugin->cls, cls->deposit. exchange_url, &wtid), "Set transfer status to verified failed\n"); TEST_RET_ON_FAIL (test_lookup_transfer (cls->deposit.exchange_url, &wtid, &total_with_fee, &transfer_data.wire_fee, &transfer_data.execution_time, true)); /* Test lookup transfer summary */ TEST_RET_ON_FAIL (test_lookup_transfer_summary (cls->deposit.exchange_url, &wtid, cls->order.id, &cls->deposit.amount_with_fee, &cls->deposit.deposit_fee)); /* Test lookup transfer details */ TEST_RET_ON_FAIL (test_lookup_transfer_details (cls->deposit.exchange_url, &wtid, 1, &transfer_detail)); /* Test lookup transfers */ full_transfer_data.details = transfer_data; TEST_RET_ON_FAIL (test_lookup_transfers (cls->instance.instance.id, cls->account.payto_uri, GNUNET_TIME_UNIT_FOREVER_ABS, GNUNET_TIME_UNIT_ZERO_ABS, 1, 0, TALER_EXCHANGE_YNA_ALL, 1, &full_transfer_data)); /* Test lookup transfer details by order */ { const struct TransferContainer transfers[] = { { .wtid = wtid, .exchange_url = cls->deposit.exchange_url, .execution_time = transfer_data.execution_time, .deposit_value = cls->deposit.amount_with_fee, .deposit_fee = cls->deposit.deposit_fee, .confirmed = true } }; TEST_RET_ON_FAIL (test_lookup_transfer_details_by_order (5, 1, transfers)); } return 0; } static int test_transfers (void *cls) { struct TestTransfers_Closure test_cls; pre_test_transfers (&test_cls); int test_result = run_test_transfers (&test_cls); post_test_transfers (&test_cls); return test_result; } /* Reserves and tips */ struct ReserveData { /** * The reserve public key */ struct TALER_ReservePublicKeyP reserve_pub; /** * The reserve private key */ struct TALER_ReservePrivateKeyP reserve_priv; /** * The reserve initial amount */ struct TALER_Amount initial_amount; /** * The exchange url */ const char *exchange_url; /** * The expiration date */ struct GNUNET_TIME_Absolute expiration; }; static int test_insert_reserve (const struct InstanceData *instance, const struct ReserveData *reserve, enum TALER_ErrorCode expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_reserve (plugin->cls, instance->instance.id, &reserve->reserve_priv, &reserve->reserve_pub, reserve->exchange_url, &reserve->initial_amount, reserve->expiration), "Insert reserve failed\n"); return 0; } struct TestLookupReserve_Closure { const struct ReserveData *reserve_to_cmp; unsigned int tips_length; const struct TALER_MERCHANTDB_TipDetails *tips; int result_matches; }; static void lookup_reserve_cb (void *cls, struct GNUNET_TIME_Absolute creation_time, struct GNUNET_TIME_Absolute expiration_time, const struct TALER_Amount *merchant_initial_amount, const struct TALER_Amount *exchange_initial_amount, const struct TALER_Amount *picked_up_amount, const struct TALER_Amount *committed_amount, bool active, unsigned int tips_length, const struct TALER_MERCHANTDB_TipDetails *tips) { struct TestLookupReserve_Closure *cmp = cls; unsigned int tip_cmp_results[tips_length]; if (NULL == cmp) return; if ((cmp->reserve_to_cmp->expiration.abs_value_us != expiration_time.abs_value_us) || (GNUNET_OK != TALER_amount_cmp_currency ( &cmp->reserve_to_cmp->initial_amount, merchant_initial_amount)) || (0 != TALER_amount_cmp (&cmp->reserve_to_cmp->initial_amount, merchant_initial_amount)) || (cmp->tips_length != tips_length)) { cmp->result_matches = 1; return; } memset (tip_cmp_results, 0, sizeof (unsigned int) * tips_length); for (unsigned int i = 0; tips_length > i; ++i) { for (unsigned int j = 0; tips_length > j; ++j) { if ((GNUNET_OK == TALER_amount_cmp_currency (&cmp->tips[i].total_amount, &tips[j].total_amount)) && (0 == TALER_amount_cmp (&cmp->tips[i].total_amount, &tips[j].total_amount)) && (0 == strcmp (cmp->tips[i].reason, tips[j].reason))) { tip_cmp_results[i] += 1; } } } for (unsigned int i = 0; tips_length > i; ++i) { if (1 != tip_cmp_results[i]) { cmp->result_matches = 1; return; } } cmp->result_matches = 0; } static int test_lookup_reserve (const char *instance_id, const struct TALER_ReservePublicKeyP *reserve_pub, const struct ReserveData *reserve) { struct TestLookupReserve_Closure cmp = { .reserve_to_cmp = reserve, .tips_length = 0, .tips = NULL, .result_matches = 0 }; if (1 != plugin->lookup_reserve (plugin->cls, instance_id, reserve_pub, false, &lookup_reserve_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup reserve failed\n"); return 1; } if (0 != cmp.result_matches) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup reserve failed: result does not match\n"); return 1; } return 0; } struct TestLookupReserves_Closure { unsigned int reserves_to_cmp_length; const struct ReserveData *reserves_to_cmp; unsigned int *results_matching; unsigned int results_length; }; static void lookup_reserves_cb (void *cls, const struct TALER_ReservePublicKeyP *reserve_pub, struct GNUNET_TIME_Absolute creation_time, struct GNUNET_TIME_Absolute expiration_time, const struct TALER_Amount *merchant_initial_amount, const struct TALER_Amount *exchange_initial_amount, const struct TALER_Amount *pickup_amount, const struct TALER_Amount *committed_amount, bool active) { struct TestLookupReserves_Closure *cmp = cls; if (NULL == cmp) return; for (unsigned int i = 0; cmp->reserves_to_cmp_length > i; ++i) { if ((0 == GNUNET_memcmp (&cmp->reserves_to_cmp[i].reserve_pub, reserve_pub)) && (cmp->reserves_to_cmp[i].expiration.abs_value_us == expiration_time.abs_value_us) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->reserves_to_cmp[i].initial_amount, merchant_initial_amount)) && (0 == TALER_amount_cmp (&cmp->reserves_to_cmp[i].initial_amount, merchant_initial_amount))) { cmp->results_matching[i] += 1; } } cmp->results_length += 1; } static int test_lookup_reserves (const char *instance_id, unsigned int reserves_length, const struct ReserveData *reserves) { unsigned int results_matching[reserves_length]; struct TestLookupReserves_Closure cmp = { .reserves_to_cmp_length = reserves_length, .reserves_to_cmp = reserves, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * reserves_length); if (1 != plugin->lookup_reserves (plugin->cls, instance_id, GNUNET_TIME_absolute_get_zero_ (), TALER_EXCHANGE_YNA_ALL, TALER_EXCHANGE_YNA_ALL, &lookup_reserves_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup reserves failed\n"); return 1; } if (reserves_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup reserves failed: incorrect number of results (%d)\n", cmp.results_length); return 1; } for (unsigned int i = 0; reserves_length > i; ++i) { if (1 != cmp.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup reserves failed: mismatched data\n"); return 1; } } return 0; } static void lookup_pending_reserves_cb (void *cls, const char *instance_id, const char *exchange_url, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_Amount *expected_amount) { struct TestLookupReserves_Closure *cmp = cls; if (NULL == cmp) return; for (unsigned int i = 0; cmp->reserves_to_cmp_length > i; ++i) { if ((0 == GNUNET_memcmp (&cmp->reserves_to_cmp[i].reserve_pub, reserve_pub)) && (0 == strcmp (cmp->reserves_to_cmp[i].exchange_url, exchange_url)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->reserves_to_cmp[i].initial_amount, expected_amount)) && (0 == TALER_amount_cmp (&cmp->reserves_to_cmp[i].initial_amount, expected_amount))) { cmp->results_matching[i] += 1; } } cmp->results_length += 1; } static int test_lookup_pending_reserves (unsigned int reserves_length, const struct ReserveData *reserves) { unsigned int results_matching[reserves_length]; struct TestLookupReserves_Closure cmp = { .reserves_to_cmp_length = reserves_length, .reserves_to_cmp = reserves, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * reserves_length); if (0 > plugin->lookup_pending_reserves (plugin->cls, &lookup_pending_reserves_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pending reserves failed\n"); return 1; } if (reserves_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pending reserves failed: incorrect number of results (%d)\n", cmp.results_length); return 1; } for (unsigned int i = 0; reserves_length > i; ++i) { if (1 != cmp.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pending reserves failed: mismatched data\n"); return 1; } } return 0; } struct TipData { struct TALER_MERCHANTDB_TipDetails details; const char *next_url; struct GNUNET_TIME_Absolute expiration; }; static void make_tip (struct TipData *tip) { GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:0.99", &tip->details.total_amount)); tip->details.reason = "because"; tip->next_url = "https://taler.net"; } static int test_authorize_tip (const struct InstanceData *instance, const struct ReserveData *reserve, struct TipData *tip) { TEST_COND_RET_ON_FAIL (TALER_EC_NONE == plugin->authorize_tip (plugin->cls, instance->instance.id, &reserve->reserve_pub, &tip->details.total_amount, tip->details.reason, tip->next_url, &tip->details.tip_id, &tip->expiration), "Authorize tip failed\n"); return 0; } static int test_lookup_tip (const struct InstanceData *instance, const struct ReserveData *reserve, const struct TipData *tip, const struct TALER_Amount *expected_total_picked_up) { struct TALER_Amount total_authorized; struct TALER_Amount total_picked_up; struct GNUNET_TIME_Absolute expiration; char *exchange_url = NULL; struct TALER_ReservePrivateKeyP reserve_priv; if (1 != plugin->lookup_tip (plugin->cls, instance->instance.id, &tip->details.tip_id, &total_authorized, &total_picked_up, &expiration, &exchange_url, &reserve_priv)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup tip failed\n"); if (NULL != exchange_url) GNUNET_free (exchange_url); return 1; } if ((GNUNET_OK != TALER_amount_cmp_currency (&tip->details.total_amount, &total_authorized)) || (0 != TALER_amount_cmp (&tip->details.total_amount, &total_authorized)) || (GNUNET_OK != TALER_amount_cmp_currency (expected_total_picked_up, &total_picked_up)) || (0 != TALER_amount_cmp (expected_total_picked_up, &total_picked_up)) || (tip->expiration.abs_value_us != expiration.abs_value_us) || (0 != strcmp (reserve->exchange_url, exchange_url)) || (0 != GNUNET_memcmp (&reserve->reserve_priv, &reserve_priv))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup tip failed: mismatched data\n"); if (NULL != exchange_url) GNUNET_free (exchange_url); return 1; } if (NULL != exchange_url) GNUNET_free (exchange_url); return 0; } static int test_lookup_tip_details (const struct InstanceData *instance, const struct ReserveData *reserve, const struct TipData *tip, const struct TALER_Amount *expected_total_picked_up, unsigned int expected_pickups_length, const struct TALER_MERCHANTDB_PickupDetails *expected_pickups) { struct TALER_Amount total_authorized; struct TALER_Amount total_picked_up; char *justification = NULL; struct GNUNET_TIME_Absolute expiration; struct TALER_ReservePublicKeyP reserve_pub; unsigned int pickups_length; struct TALER_MERCHANTDB_PickupDetails *pickups = NULL; unsigned int results_matching[expected_pickups_length]; if (TALER_EC_NONE != plugin->lookup_tip_details (plugin->cls, instance->instance.id, &tip->details.tip_id, true, &total_authorized, &total_picked_up, &justification, &expiration, &reserve_pub, &pickups_length, &pickups)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup tip details failed\n"); if (NULL != justification) GNUNET_free (justification); if (NULL != pickups) GNUNET_free (pickups); return 1; } if ((GNUNET_OK != TALER_amount_cmp_currency (&tip->details.total_amount, &total_authorized)) || (0 != TALER_amount_cmp (&tip->details.total_amount, &total_authorized)) || (GNUNET_OK != TALER_amount_cmp_currency (expected_total_picked_up, &total_picked_up)) || (0 != TALER_amount_cmp (expected_total_picked_up, &total_picked_up)) || (0 != strcmp (tip->details.reason, justification)) || (tip->expiration.abs_value_us != expiration.abs_value_us) || (0 != GNUNET_memcmp (&reserve->reserve_pub, &reserve_pub)) || (expected_pickups_length != pickups_length)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup tip details failed: mismatched data\n"); if (NULL != justification) GNUNET_free (justification); if (NULL != pickups) GNUNET_free (pickups); return 1; } memset (results_matching, 0, sizeof (unsigned int) * expected_pickups_length); for (unsigned int i = 0; expected_pickups_length > i; ++i) { for (unsigned int j = 0; pickups_length > j; ++j) { /* Compare expected_pickups[i] with pickups[j] */ if ((0 == GNUNET_memcmp (&expected_pickups[i].pickup_id, &pickups[j].pickup_id)) && (GNUNET_OK == TALER_amount_cmp_currency ( &expected_pickups[i].requested_amount, &pickups[j].requested_amount)) && (0 == TALER_amount_cmp (&expected_pickups[i].requested_amount, &pickups[j].requested_amount)) && (expected_pickups[i].num_planchets == pickups[j].num_planchets)) { results_matching[i] += 1; } } } for (unsigned int i = 0; expected_pickups_length > i; ++i) { if (1 != results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup tip details failed: mismatched data\n"); if (NULL != justification) GNUNET_free (justification); if (NULL != pickups) GNUNET_free (pickups); return 1; } } return 0; } static void free_rsa_signature_array (unsigned int sigs_length, struct GNUNET_CRYPTO_RsaSignature **sigs) { for (unsigned int i = 0; sigs_length > i; ++i) { if (NULL != sigs[i]) GNUNET_free (sigs[i]); } } static int test_lookup_pickup (const char *instance_id, const struct GNUNET_HashCode *tip_id, const struct GNUNET_HashCode *pickup_id, const char *expected_exchange_url, const struct TALER_ReservePrivateKeyP *expected_reserve_priv, unsigned int expected_sigs_length, const struct GNUNET_CRYPTO_RsaSignature **expected_sigs) { char *exchange_url = NULL; struct TALER_ReservePrivateKeyP reserve_priv; struct GNUNET_CRYPTO_RsaSignature *sigs[expected_sigs_length]; unsigned int results_matching[expected_sigs_length]; memset (sigs, 0, sizeof (struct GNUNET_CRYPTO_RsaSignature *) * expected_sigs_length); if (0 > plugin->lookup_pickup (plugin->cls, instance_id, tip_id, pickup_id, &exchange_url, &reserve_priv, expected_sigs_length, sigs)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pickup failed\n"); if (NULL != exchange_url) GNUNET_free (exchange_url); free_rsa_signature_array (expected_sigs_length, sigs); return 1; } if ((0 != strcmp (expected_exchange_url, exchange_url)) || (0 != GNUNET_memcmp (expected_reserve_priv, &reserve_priv))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pickup failed: mismatched data\n"); if (NULL != exchange_url) GNUNET_free (exchange_url); free_rsa_signature_array (expected_sigs_length, sigs); return 1; } memset (results_matching, 0, sizeof (unsigned int) * expected_sigs_length); for (unsigned int i = 0; expected_sigs_length > i; ++i) { for (unsigned int j = 0; expected_sigs_length > j; ++j) { /* compare expected_sigs[i] to sigs[j] */ if (0 == GNUNET_CRYPTO_rsa_signature_cmp (expected_sigs[i], sigs[j])) { results_matching[i] += 1; } } } for (unsigned int i = 0; expected_sigs_length > i; ++i) { if (1 != results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pickup failed: mismatched data\n"); if (NULL != exchange_url) GNUNET_free (exchange_url); free_rsa_signature_array (expected_sigs_length, sigs); return 1; } } return 0; } struct TestLookupTips_Closure { unsigned int tips_to_cmp_length; const struct TipData *tips_to_cmp; unsigned int results_length; bool *results_match; }; static void lookup_tips_cb (void *cls, uint64_t row_id, struct GNUNET_HashCode tip_id, struct TALER_Amount amount) { struct TestLookupTips_Closure *cmp = cls; if (NULL == cmp) return; unsigned int i = cmp->results_length; cmp->results_length += 1; if (cmp->tips_to_cmp_length > i) { if ((0 == GNUNET_memcmp (&cmp->tips_to_cmp[i].details.tip_id, &tip_id)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->tips_to_cmp[i].details.total_amount, &amount)) && (0 == TALER_amount_cmp (&cmp->tips_to_cmp[i].details.total_amount, &amount))) cmp->results_match[i] = true; else cmp->results_match[i] = false; } } static int test_lookup_tips (const struct InstanceData *instance, enum TALER_EXCHANGE_YesNoAll expired, int64_t limit, uint64_t offset, unsigned int tips_length, const struct TipData *tips) { bool results_match[tips_length]; struct TestLookupTips_Closure cmp = { .tips_to_cmp_length = tips_length, .tips_to_cmp = tips, .results_length = 0, .results_match = results_match }; memset (results_match, 0, sizeof (bool) * tips_length); if (0 > plugin->lookup_tips (plugin->cls, instance->instance.id, expired, limit, offset, &lookup_tips_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup tips failed\n"); return 1; } if (tips_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup tips failed: incorrect number of results (%d)\n", cmp.results_length); return 1; } for (unsigned int i = 0; i < tips_length; ++i) { if (true != cmp.results_match[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup tips failed: mismatched data\n"); return 1; } } return 0; } /** * Convenience function for testing lookup tips with filters */ static void reverse_tip_data_array (unsigned int tips_length, struct TipData *tips) { struct TipData tmp[tips_length]; for (unsigned int i = 0; i < tips_length; ++i) tmp[i] = tips[tips_length - 1 - i]; for (unsigned int i = 0; i < tips_length; ++i) tips[i] = tmp[i]; } struct TestTips_Closure { struct InstanceData instance; /** * The tip reserve data */ struct ReserveData reserve; struct ReserveData expired_reserve; /* Tip and pickup data */ struct TipData tip; struct TipData bigtip; struct TipData tips[5]; struct GNUNET_HashCode pickup_id; struct GNUNET_CRYPTO_RsaPrivateKey *pickup_priv; struct GNUNET_CRYPTO_RsaSignature *pickup_sig; }; static void pre_test_tips (struct TestTips_Closure *cls) { /* Instance */ make_instance ("test_inst_tips", &cls->instance); /* Reserve */ GNUNET_CRYPTO_eddsa_key_create (&cls->reserve.reserve_priv.eddsa_priv); GNUNET_CRYPTO_eddsa_key_get_public (&cls->reserve.reserve_priv.eddsa_priv, &cls->reserve.reserve_pub.eddsa_pub); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:99.99", &cls->reserve.initial_amount)); cls->reserve.exchange_url = "exch-url"; cls->reserve.expiration = GNUNET_TIME_absolute_add (GNUNET_TIME_absolute_get (), GNUNET_TIME_UNIT_WEEKS); GNUNET_CRYPTO_eddsa_key_create ( &cls->expired_reserve.reserve_priv.eddsa_priv); GNUNET_CRYPTO_eddsa_key_get_public ( &cls->expired_reserve.reserve_priv.eddsa_priv, &cls->expired_reserve.reserve_pub. eddsa_pub); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:99.99", &cls->expired_reserve.initial_amount)); cls->expired_reserve.exchange_url = "exch-url"; cls->expired_reserve.expiration = GNUNET_TIME_UNIT_ZERO_ABS; /* Tip/pickup */ make_tip (&cls->tip); make_tip (&cls->bigtip); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:99.90", &cls->bigtip.details.total_amount)); for (unsigned int i = 0; i < 5; ++i) { char amount[16]; make_tip (&cls->tips[i]); GNUNET_snprintf (amount, 16, "EUR:0.%u0", i + 1); GNUNET_assert (GNUNET_OK == TALER_string_to_amount (amount, &cls->tips[i].details.total_amount)); } cls->pickup_priv = GNUNET_CRYPTO_rsa_private_key_create (2048); cls->pickup_sig = GNUNET_CRYPTO_rsa_sign_fdh (cls->pickup_priv, &cls->pickup_id); } static void post_test_tips (struct TestTips_Closure *cls) { free_instance_data (&cls->instance); GNUNET_CRYPTO_rsa_private_key_free (cls->pickup_priv); GNUNET_CRYPTO_rsa_signature_free (cls->pickup_sig); } static int run_test_tips (struct TestTips_Closure *cls) { struct TALER_Amount zero; TALER_amount_get_zero ("EUR", &zero); TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test insert reserve */ TEST_RET_ON_FAIL (test_insert_reserve (&cls->instance, &cls->reserve, TALER_EC_NONE)); /* Test lookup reserve */ TEST_RET_ON_FAIL (test_lookup_reserve (cls->instance.instance.id, &cls->reserve.reserve_pub, &cls->reserve)); /* Test lookup pending reserves */ TEST_RET_ON_FAIL (test_lookup_pending_reserves (1, &cls->reserve)); /* Test reserve activation */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->activate_reserve (plugin->cls, cls->instance.instance.id, &cls->reserve.reserve_pub, &cls->reserve.initial_amount), "Activate reserve failed\n"); TEST_RET_ON_FAIL (test_lookup_pending_reserves (0, NULL)); /* Test inserting a tip */ TEST_RET_ON_FAIL (test_authorize_tip (&cls->instance, &cls->reserve, &cls->tip)); /* Test lookup tip */ TEST_RET_ON_FAIL (test_lookup_tip (&cls->instance, &cls->reserve, &cls->tip, &zero)); /* Test lookup tip details */ TEST_RET_ON_FAIL (test_lookup_tip_details (&cls->instance, &cls->reserve, &cls->tip, &zero, 0, NULL)); /* Test insert pickup */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->insert_pickup (plugin->cls, cls->instance.instance.id, &cls->tip.details.tip_id, &cls->tip.details.total_amount, &cls->pickup_id, &cls->tip.details.total_amount), "Insert pickup failed\n"); /* Test lookup pickup */ TEST_RET_ON_FAIL (test_lookup_pickup (cls->instance.instance.id, &cls->tip.details.tip_id, &cls->pickup_id, cls->reserve.exchange_url, &cls->reserve.reserve_priv, 0, NULL)); /* Test insert pickup blind signature */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->insert_pickup_blind_signature (plugin->cls, &cls->pickup_id, 0, cls->pickup_sig), "Insert pickup blind signature failed\n"); /* Test that overdrawing the reserve fails */ TEST_COND_RET_ON_FAIL (TALER_EC_NONE != plugin->authorize_tip (plugin->cls, cls->instance.instance.id, &cls->reserve.reserve_pub, &cls->bigtip.details. total_amount, cls->bigtip.details.reason, cls->bigtip.next_url, &cls->bigtip.details.tip_id, &cls->reserve.expiration), "Authorize tip failed\n"); /* Test lookup tips */ TEST_RET_ON_FAIL (test_lookup_tips (&cls->instance, TALER_EXCHANGE_YNA_ALL, 1, 0, 1, &cls->tip)); /* Test lookup reserves */ TEST_RET_ON_FAIL (test_lookup_reserves (cls->instance.instance.id, 1, &cls->reserve)); { /* Test lookup tips & friends */ struct TipData expected_tips[6]; expected_tips[0] = cls->tip; TEST_RET_ON_FAIL (test_insert_reserve (&cls->instance, &cls->expired_reserve, TALER_EC_NONE)); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->activate_reserve (plugin->cls, cls->instance.instance.id, &cls->expired_reserve. reserve_pub, &cls->expired_reserve. initial_amount), "Activate reserve failed\n"); for (unsigned int i = 0; i < 5; ++i) { if (i % 2 == 0) { TEST_RET_ON_FAIL (test_authorize_tip (&cls->instance, &cls->expired_reserve, &cls->tips[i])); } else { TEST_RET_ON_FAIL (test_authorize_tip (&cls->instance, &cls->reserve, &cls->tips[i])); } } GNUNET_memcpy (&expected_tips[1], cls->tips, sizeof (struct TipData) * 5); /* Test lookup tips inc */ TEST_RET_ON_FAIL (test_lookup_tips (&cls->instance, TALER_EXCHANGE_YNA_ALL, 6, 0, 6, expected_tips)); reverse_tip_data_array (6, expected_tips); /* Test lookup tips dec */ TEST_RET_ON_FAIL (test_lookup_tips (&cls->instance, TALER_EXCHANGE_YNA_ALL, -6, 10, 6, expected_tips)); /* Test lookup tips expired inc */ expected_tips[0] = cls->tips[0]; expected_tips[1] = cls->tips[2]; expected_tips[2] = cls->tips[4]; TEST_RET_ON_FAIL (test_lookup_tips (&cls->instance, TALER_EXCHANGE_YNA_YES, 6, 0, 3, expected_tips)); /* Test lookup tips expired dec */ reverse_tip_data_array (3, expected_tips); TEST_RET_ON_FAIL (test_lookup_tips (&cls->instance, TALER_EXCHANGE_YNA_YES, -6, 10, 3, expected_tips)); /* Test lookup tips unexpired inc */ expected_tips[0] = cls->tip; expected_tips[1] = cls->tips[1]; expected_tips[2] = cls->tips[3]; TEST_RET_ON_FAIL (test_lookup_tips (&cls->instance, TALER_EXCHANGE_YNA_NO, 6, 0, 3, expected_tips)); /* Test lookup tips unexpired dec */ reverse_tip_data_array (3, expected_tips); TEST_RET_ON_FAIL (test_lookup_tips (&cls->instance, TALER_EXCHANGE_YNA_NO, -6, 10, 3, expected_tips)); } /* Test delete reserve private key */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->delete_reserve (plugin->cls, cls->instance.instance.id, &cls->reserve.reserve_pub), "Delete reserve private key failed\n"); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->delete_reserve (plugin->cls, cls->instance.instance.id, &cls->reserve.reserve_pub), "Delete reserve private key failed\n"); /* Test purging a reserve */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->purge_reserve (plugin->cls, cls->instance.instance.id, &cls->reserve.reserve_pub), "Purge reserve failed\n"); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->purge_reserve (plugin->cls, cls->instance.instance.id, &cls->reserve.reserve_pub), "Purge reserve failed\n"); return 0; } static int test_tips (void *cls) { struct TestTips_Closure test_cls; pre_test_tips (&test_cls); int test_result = run_test_tips (&test_cls); post_test_tips (&test_cls); return test_result; } struct TestLookupRefunds_Closure { unsigned int refunds_to_cmp_length; const struct TALER_CoinSpendPublicKeyP *coin_pub_to_cmp; const struct TALER_Amount *refund_amount_to_cmp; unsigned int *results_matching; unsigned int results_length; }; static void lookup_refunds_cb (void *cls, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *refund_amount) { struct TestLookupRefunds_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; cmp->refunds_to_cmp_length > i; ++i) { if ((0 == GNUNET_memcmp (&cmp->coin_pub_to_cmp[i], coin_pub)) && (GNUNET_OK == TALER_amount_cmp_currency (&cmp->refund_amount_to_cmp[i], refund_amount)) && (0 == TALER_amount_cmp (&cmp->refund_amount_to_cmp[i], refund_amount))) { cmp->results_matching[i] += 1; } } } static int test_lookup_refunds (const char *instance_id, const struct GNUNET_HashCode *h_contract_terms, unsigned int refunds_length, const struct TALER_CoinSpendPublicKeyP *coin_pubs, const struct TALER_Amount *refund_amounts) { unsigned int results_matching[refunds_length]; struct TestLookupRefunds_Closure cmp = { .refunds_to_cmp_length = refunds_length, .coin_pub_to_cmp = coin_pubs, .refund_amount_to_cmp = refund_amounts, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * refunds_length); if (1 != plugin->lookup_refunds (plugin->cls, instance_id, h_contract_terms, &lookup_refunds_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup refunds failed\n"); return 1; } if (refunds_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup refunds failed: incorrect number of results returned\n"); return 1; } for (unsigned int i = 0; refunds_length > i; ++i) { if (1 != cmp.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup refunds failed: mismatched data\n"); return 1; } } return 0; } struct RefundData { struct GNUNET_TIME_Absolute timestamp; const char *reason; struct TALER_Amount refund_amount; const struct TALER_CoinSpendPublicKeyP *coin_pub; const char *exchange_url; }; static void make_refund (const struct DepositData *deposit, struct RefundData *refund) { refund->timestamp = GNUNET_TIME_absolute_get (); refund->reason = "some reason"; refund->refund_amount = deposit->amount_with_fee; refund->coin_pub = &deposit->coin_pub; refund->exchange_url = deposit->exchange_url; } struct RefundProofData { struct TALER_Amount refund_fee; struct TALER_ExchangeSignatureP exchange_sig; }; struct TestLookupRefundsDetailed_Closure { unsigned int refunds_to_cmp_length; const struct RefundData *refunds_to_cmp; unsigned int *results_matching; unsigned int results_length; }; static void lookup_refunds_detailed_cb (void *cls, uint64_t refund_serial, struct GNUNET_TIME_Absolute timestamp, const struct TALER_CoinSpendPublicKeyP *coin_pub, const char *exchange_url, uint64_t rtransaction_id, const char *reason, const struct TALER_Amount *refund_amount) { struct TestLookupRefundsDetailed_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; cmp->refunds_to_cmp_length > i; ++i) { if ((cmp->refunds_to_cmp[i].timestamp.abs_value_us == timestamp.abs_value_us) && (0 == GNUNET_memcmp (cmp->refunds_to_cmp[i].coin_pub, coin_pub)) && (0 == strcmp (cmp->refunds_to_cmp[i].exchange_url, exchange_url)) && (0 == strcmp (cmp->refunds_to_cmp[i].reason, reason)) && (GNUNET_OK == TALER_amount_cmp_currency ( &cmp->refunds_to_cmp[i].refund_amount, refund_amount)) && (0 == TALER_amount_cmp (&cmp->refunds_to_cmp[i].refund_amount, refund_amount))) { cmp->results_matching[i] += 1; } } } static int test_lookup_refunds_detailed (const char *instance_id, const struct GNUNET_HashCode *h_contract_terms, unsigned int refunds_length, const struct RefundData *refunds) { unsigned int results_matching[refunds_length]; struct TestLookupRefundsDetailed_Closure cmp = { .refunds_to_cmp_length = refunds_length, .refunds_to_cmp = refunds, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * refunds_length); if (1 != plugin->lookup_refunds_detailed (plugin->cls, instance_id, h_contract_terms, &lookup_refunds_detailed_cb, &cmp)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup refunds detailed failed\n"); return 1; } if (refunds_length != cmp.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup refunds detailed failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; refunds_length > i; ++i) { if (1 != cmp.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup refunds detailed failed: mismatched data\n"); return 1; } } return 0; } static int test_lookup_refund_proof (uint64_t refund_serial, const struct TALER_ExchangeSignatureP *expected_exchange_sig, const struct TALER_ExchangePublicKeyP *expected_exchange_pub) { struct TALER_ExchangeSignatureP exchange_sig; struct TALER_ExchangePublicKeyP exchange_pub; if (1 != plugin->lookup_refund_proof (plugin->cls, refund_serial, &exchange_sig, &exchange_pub)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup refund proof failed\n"); return 1; } if ((0 != GNUNET_memcmp (expected_exchange_sig, &exchange_sig)) || (0 != GNUNET_memcmp (expected_exchange_pub, &exchange_pub))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup refund proof failed: mismatched data\n"); return 1; } return 0; } struct TestRefunds_Closure { struct InstanceData instance; /** * The merchant account */ struct TALER_MERCHANTDB_AccountDetails account; /** * The exchange signing key */ struct ExchangeSignkeyData signkey; /** * The order data */ struct OrderData order; /** * The deposit data */ struct DepositData deposits[2]; /** * The refund data */ struct RefundData refund; /** * The refund proof data */ struct RefundProofData refund_proof; }; static void pre_test_refunds (struct TestRefunds_Closure *cls) { struct TALER_RefundRequestPS refund_sign = { .purpose = { .size = htonl (sizeof (struct TALER_RefundRequestPS)), .purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND) } }; /* Instance */ make_instance ("test_inst_refunds", &cls->instance); /* Account */ make_account (&cls->account); /* Signing key */ make_exchange_signkey (&cls->signkey); /* Order */ make_order ("test_refunds_od_0", &cls->order); /* Deposit */ make_deposit (&cls->instance, &cls->account, &cls->order, &cls->signkey, &cls->deposits[0]); make_deposit (&cls->instance, &cls->account, &cls->order, &cls->signkey, &cls->deposits[1]); /* Refund */ make_refund (&cls->deposits[0], &cls->refund); /* Refund proof */ GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:0.02", &cls->refund_proof.refund_fee)); GNUNET_CRYPTO_eddsa_sign (&cls->signkey.exchange_priv.eddsa_priv, &refund_sign, &cls->refund_proof.exchange_sig.eddsa_signature); } static void post_test_refunds (struct TestRefunds_Closure *cls) { free_instance_data (&cls->instance); free_order_data (&cls->order); } static int run_test_refunds (struct TestRefunds_Closure *cls) { struct TALER_Amount inc; /* Insert an instance */ TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert an account */ TEST_RET_ON_FAIL (test_insert_account (&cls->instance, &cls->account, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert an order */ TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->order, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert contract terms */ TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->order, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert a signing key */ TEST_RET_ON_FAIL (test_insert_exchange_signkey (&cls->signkey, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert a deposit */ TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposits[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposits[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Mark as paid */ TEST_RET_ON_FAIL (test_mark_contract_paid (&cls->instance, &cls->order, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test refund coin */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->refund_coin (plugin->cls, cls->instance.instance.id, &cls->deposits[0].h_contract_terms, cls->refund.timestamp, cls->refund.coin_pub, cls->refund.reason), "Refund coin failed\n"); /* Test double refund fails */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->refund_coin (plugin->cls, cls->instance.instance.id, &cls->deposits[0].h_contract_terms, cls->refund.timestamp, cls->refund.coin_pub, cls->refund.reason), "Refund coin failed\n"); /* Test lookup refunds */ TEST_RET_ON_FAIL (test_lookup_refunds (cls->instance.instance.id, &cls->deposits[0].h_contract_terms, 1, cls->refund.coin_pub, &cls->refund.refund_amount)); /* Test lookup refunds detailed */ TEST_RET_ON_FAIL (test_lookup_refunds_detailed (cls->instance.instance.id, &cls->deposits[0]. h_contract_terms, 1, &cls->refund)); /* Test insert refund proof */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->insert_refund_proof (plugin->cls, 1, // TODO: get this from lookup &cls->refund_proof. refund_fee, &cls->refund_proof. exchange_sig, &cls->signkey.exchange_pub), "Insert refund proof failed\n"); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->insert_refund_proof (plugin->cls, 1, // TODO: get this from lookup &cls->refund_proof. refund_fee, &cls->refund_proof. exchange_sig, &cls->signkey.exchange_pub), "Insert refund proof failed\n"); /* Test that we can't give too much in refunds */ GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:1000.00", &inc)); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->increase_refund (plugin->cls, cls->instance.instance.id, cls->order.id, &inc, "more"), "Increase refund failed\n"); /* Test increase refund */ GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:1.00", &inc)); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->increase_refund (plugin->cls, cls->instance.instance.id, cls->order.id, &inc, "more"), "Increase refund failed\n"); /* Test lookup refund proof */ TEST_RET_ON_FAIL (test_lookup_refund_proof (1, &cls->refund_proof.exchange_sig, &cls->signkey.exchange_pub)); return 0; } static int test_refunds (void *cls) { struct TestRefunds_Closure test_cls; pre_test_refunds (&test_cls); int test_result = run_test_refunds (&test_cls); post_test_refunds (&test_cls); return test_result; } /** * Convenience function for testing lookup orders with filters */ static void reverse_order_data_array (unsigned int orders_length, struct OrderData *orders) { struct OrderData tmp[orders_length]; for (unsigned int i = 0; i < orders_length; ++i) tmp[i] = orders[orders_length - 1 - i]; for (unsigned int i = 0; i < orders_length; ++i) orders[i] = tmp[i]; } struct TestLookupOrdersAllFilters_Closure { struct InstanceData instance; struct TALER_MERCHANTDB_AccountDetails account; struct ExchangeSignkeyData signkey; char *order_ids[64]; struct OrderData orders[64]; struct DepositData deposits[64]; struct RefundData refunds[64]; }; static void pre_test_lookup_orders_all_filters (struct TestLookupOrdersAllFilters_Closure *cls) { make_instance ("test_inst_lookup_orders_all_filters", &cls->instance); make_account (&cls->account); make_exchange_signkey (&cls->signkey); for (unsigned int i = 0; i < 64; ++i) { (void) GNUNET_asprintf (&cls->order_ids[i], "test_orders_filter_od_%u", i); make_order (cls->order_ids[i], &cls->orders[i]); GNUNET_assert (0 == json_object_set (cls->orders[i].contract, "order_id", json_string (cls->order_ids[i]))); make_deposit (&cls->instance, &cls->account, &cls->orders[i], &cls->signkey, &cls->deposits[i]); make_refund (&cls->deposits[i], &cls->refunds[i]); } } static void post_test_lookup_orders_all_filters (struct TestLookupOrdersAllFilters_Closure *cls) { free_instance_data (&cls->instance); for (unsigned int i = 0; i < 64; ++i) { free_order_data (&cls->orders[i]); GNUNET_free (cls->order_ids[i]); } } static int run_test_lookup_orders_all_filters (struct TestLookupOrdersAllFilters_Closure *cls) { /* Order filter extravaganza */ struct { bool claimed; bool paid; bool refunded; bool wired; } order_status[64]; unsigned int *permutation; /* Pseudorandomly generate variations for the filter to differentiate */ GNUNET_CRYPTO_seed_weak_random (1); permutation = GNUNET_CRYPTO_random_permute (GNUNET_CRYPTO_QUALITY_WEAK, 64); for (unsigned int i = 0; i < 64; ++i) { unsigned int dest = permutation[i]; order_status[dest].claimed = (i & 1) ? true : false; order_status[dest].paid = (3 == (i & 3)) ? true : false; order_status[dest].refunded = (5 == (i & 5)) ? true : false; order_status[dest].wired = (9 == (i & 9)) ? true : false; } GNUNET_free (permutation); TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_account (&cls->instance, &cls->account, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_exchange_signkey (&cls->signkey, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); for (unsigned int i = 0; i < 64; ++i) { TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[i], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); if (order_status[i].claimed) { TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->orders[i], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); } else { continue; } if (order_status[i].paid) TEST_RET_ON_FAIL (test_mark_contract_paid (&cls->instance, &cls->orders[i], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); if (order_status[i].refunded) { TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposits[i], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->refund_coin (plugin->cls, cls->instance.instance.id, &cls->deposits[i]. h_contract_terms, cls->refunds[i].timestamp, cls->refunds[i].coin_pub, cls->refunds[i].reason), "Refund coin failed\n"); } if (order_status[i].wired) TEST_RET_ON_FAIL (test_mark_order_wired (7 + i, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); } /* There are 3^3 = 27 possibilities here, not counting inc/dec and start row */ for (unsigned int i = 0; i < 27; ++i) { struct TALER_MERCHANTDB_OrderFilter filter = { .paid = (i % 3) + 1, .refunded = ((i / 3) % 3) + 1, .wired = ((i / 9) % 3) + 1, .date = GNUNET_TIME_absolute_get_zero_ (), .start_row = 0, .delta = 64 }; unsigned int orders_length = 0; struct OrderData orders[64]; /* Now figure out which orders should make it through the filter */ for (unsigned int j = 0; j < 64; ++j) { if (((TALER_EXCHANGE_YNA_YES == filter.paid) && (true != order_status[j].paid)) || ((TALER_EXCHANGE_YNA_NO == filter.paid) && (false != order_status[j].paid)) || ((TALER_EXCHANGE_YNA_YES == filter.refunded) && (true != order_status[j].refunded)) || ((TALER_EXCHANGE_YNA_NO == filter.refunded) && (false != order_status[j].refunded)) || ((TALER_EXCHANGE_YNA_YES == filter.wired) && (true != order_status[j].wired)) || ((TALER_EXCHANGE_YNA_NO == filter.wired) && (false != order_status[j].wired))) continue; orders[orders_length] = cls->orders[j]; orders_length += 1; } /* Test the lookup */ TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, &filter, orders_length, orders)); /* Now test decreasing */ filter.start_row = 256; filter.date = GNUNET_TIME_UNIT_FOREVER_ABS; filter.delta = -64; reverse_order_data_array (orders_length, orders); TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, &filter, orders_length, orders)); } return 0; } /* This has to happen after everything else because it depends on deposits, etc. */ static int test_lookup_orders_all_filters (void *cls) { struct TestLookupOrdersAllFilters_Closure test_cls; pre_test_lookup_orders_all_filters (&test_cls); int test_result = run_test_lookup_orders_all_filters (&test_cls); post_test_lookup_orders_all_filters (&test_cls); return test_result; } /** * Function that runs all tests and returns 1 upon error, 0 on success. */ static int run_tests (void *cls) { TEST_RET_ON_FAIL (test_instances ()); TEST_RET_ON_FAIL (test_products ()); TEST_RET_ON_FAIL (test_orders ()); TEST_RET_ON_FAIL (test_deposits (cls)); TEST_RET_ON_FAIL (test_transfers (cls)); TEST_RET_ON_FAIL (test_tips (cls)); TEST_RET_ON_FAIL (test_refunds (cls)); TEST_RET_ON_FAIL (test_lookup_orders_all_filters (cls)); return 0; } /** * Main function that will be run by the scheduler. * * @param cls closure with config */ static void run (void *cls) { struct GNUNET_CONFIGURATION_Handle *cfg = cls; /* Data for 'store_payment()' */ /* Drop the tables to cleanup anything that might cause issues */ if (NULL == (plugin = TALER_MERCHANTDB_plugin_load (cfg))) { result = 77; return; } GNUNET_break (GNUNET_OK == plugin->drop_tables (plugin->cls)); TALER_MERCHANTDB_plugin_unload (plugin); /* Load the plugin */ if (NULL == (plugin = TALER_MERCHANTDB_plugin_load (cfg))) { result = 77; return; } /* Run the preflight */ plugin->preflight (plugin->cls); result = run_tests (cls); /* Test dropping tables */ /*if (GNUNET_OK != plugin->drop_tables (plugin->cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Dropping tables failed\n"); result = 77; return; }*/ /* Unload the plugin */ TALER_MERCHANTDB_plugin_unload (plugin); if (NULL == (plugin = TALER_MERCHANTDB_plugin_load (cfg))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Plugin unload failed\n"); result = 77; return; } /*GNUNET_break (GNUNET_OK == plugin->drop_tables (plugin->cls));*/ TALER_MERCHANTDB_plugin_unload (plugin); plugin = NULL; } int main (int argc, char *const argv[]) { const char *plugin_name; char *config_filename; char *testname; struct GNUNET_CONFIGURATION_Handle *cfg; result = -1; if (NULL == (plugin_name = strrchr (argv[0], (int) '-'))) { GNUNET_break (0); return -1; } GNUNET_log_setup (argv[0], "DEBUG", NULL); plugin_name++; (void) GNUNET_asprintf (&testname, "test-merchantdb-%s", plugin_name); (void) GNUNET_asprintf (&config_filename, "%s.conf", testname); cfg = GNUNET_CONFIGURATION_create (); if (GNUNET_OK != GNUNET_CONFIGURATION_parse (cfg, config_filename)) { GNUNET_break (0); GNUNET_free (config_filename); GNUNET_free (testname); return 2; } GNUNET_SCHEDULER_run (&run, cfg); GNUNET_CONFIGURATION_destroy (cfg); GNUNET_free (config_filename); GNUNET_free (testname); return result; } /* end of test_merchantdb.c */