/* 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 purging all data for an instance from the database. * * @param instance the instance to purge. * @param expected_result the result we expect the db to return. * @return 0 on success, 1 otherwise. */ static int test_purge_instance (const struct InstanceData *instance, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->purge_instance (plugin->cls, instance->instance.id), "Purge instance 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 deactivate. * @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), "Deactivate 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 deactivate 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])); TEST_RET_ON_FAIL (test_purge_instance (&cls->instances[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_purge_instance (&cls->instances[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test that the instance is gone. */ TEST_RET_ON_FAIL (test_lookup_instances (false, 1, instances)); return 0; } /** * Function that tests instances. * * @return 0 on success, 1 otherwise. */ static int test_instances (void) { 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) { GNUNET_free (pd->description); if (NULL != pd->description_i18n) json_decref (pd->description_i18n); 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 = 100; cls->products[0].product.total_sold = 10; cls->products[0].product.total_lost = 7; GNUNET_assert (0 == json_array_append_new (cls->products[0].product.image, json_string ( "http://some-website.com/image.png"))); GNUNET_assert (0 == json_array_append_new (cls->products[0].product.address, json_string ("444 Some Street"))); cls->products[0].product.next_restock = GNUNET_TIME_absolute_get (); TEST_RET_ON_FAIL (test_update_product (&cls->instance, &cls->products[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); { struct ProductData stock_dec = cls->products[0]; stock_dec.product.total_stock = 40; TEST_RET_ON_FAIL (test_update_product (&cls->instance, &stock_dec, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); } { struct ProductData sold_dec = cls->products[0]; sold_dec.product.total_sold = 5; TEST_RET_ON_FAIL (test_update_product (&cls->instance, &sold_dec, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); } { struct ProductData lost_dec = cls->products[0]; lost_dec.product.total_lost = 1; TEST_RET_ON_FAIL (test_update_product (&cls->instance, &lost_dec, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); } 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 (0 != plugin->lock_product (plugin->cls, cls->instance.instance.id, cls->products[0].id, &uuid, 256, refund_deadline)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lock product failed\n"); return 1; } 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; } if (0 != 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 (void) { 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; /** * The claim token for the order. */ struct TALER_ClaimTokenP claim_token; }; /** * 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 refund_deadline; order->id = order_id; order->contract = json_object (); GNUNET_assert (NULL != order->contract); order->pay_deadline = GNUNET_TIME_absolute_add (GNUNET_TIME_absolute_get (), GNUNET_TIME_UNIT_DAYS); GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, &order->claim_token, sizeof (order->claim_token)); refund_deadline = GNUNET_TIME_absolute_add (GNUNET_TIME_absolute_get (), GNUNET_TIME_UNIT_WEEKS); GNUNET_TIME_round_abs (&order->pay_deadline); GNUNET_TIME_round_abs (&refund_deadline); json_object_set_new (order->contract, "fulfillment_url", json_string ("a")); json_object_set_new (order->contract, "order_id", json_string (order_id)); json_object_set_new (order->contract, "pay_deadline", GNUNET_JSON_from_time_abs (order->pay_deadline)); json_object_set_new (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) { struct GNUNET_HashCode h_post; memset (&h_post, 42, sizeof (h_post)); TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_order (plugin->cls, instance->instance.id, order->id, &h_post, order->pay_deadline, &order->claim_token, 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) { struct TALER_ClaimTokenP ct; json_t *lookup_terms = NULL; struct GNUNET_HashCode oh; struct GNUNET_HashCode wh; memset (&wh, 42, sizeof (wh)); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->lookup_order (plugin->cls, instance->instance.id, order->id, &ct, &oh, &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)) || (0 != GNUNET_memcmp (&order->claim_token, &ct)) || (0 != GNUNET_memcmp (&oh, &wh)) ) { 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; } /** * Container for data used for looking up the row number of an order. */ struct LookupOrderSerial_Closure { /** * The order that is being looked up. */ const struct OrderData *order; /** * The serial of the order that was found. */ uint64_t serial; }; /** * 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 get_order_serial_cb (void *cls, const char *order_id, uint64_t order_serial, struct GNUNET_TIME_Absolute timestamp) { struct LookupOrderSerial_Closure *lookup_cls = cls; if (NULL == lookup_cls) return; if (0 == strcmp (lookup_cls->order->id, order_id)) lookup_cls->serial = order_serial; } /** * Convenience function for getting the row number of an order. * * @param instance the instance to look up from. * @param order the order to lookup the serial for. * @return the row number of the order. */ static uint64_t get_order_serial (const struct InstanceData *instance, const struct OrderData *order) { struct LookupOrderSerial_Closure lookup_cls = { .order = order, .serial = 0 }; struct TALER_MERCHANTDB_OrderFilter filter = { .paid = TALER_EXCHANGE_YNA_ALL, .refunded = TALER_EXCHANGE_YNA_ALL, .wired = TALER_EXCHANGE_YNA_ALL, .date = GNUNET_TIME_UNIT_ZERO_ABS, .start_row = 0, .delta = 256 }; GNUNET_assert (0 < plugin->lookup_orders (plugin->cls, instance->instance.id, &filter, &get_order_serial_cb, &lookup_cls)); GNUNET_assert (0 != lookup_cls.serial); return lookup_cls.serial; } /** * 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; } /** * Test updating 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_update_contract_terms (const struct InstanceData *instance, const struct OrderData *order, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->update_contract_terms (plugin->cls, instance->instance.id, order->id, order->contract), "Update 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 legal_expiration how long we must wait after creating an order to delete it * @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, struct GNUNET_TIME_Relative legal_expiration, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->delete_contract_terms (plugin->cls, instance->instance.id, order->id, legal_expiration), "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_contract_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_contract_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[3]; }; /** * 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]); make_order ("test_orders_od_2", &cls->orders[2]); GNUNET_assert (0 == json_object_set_new (cls->orders[1].contract, "other_field", json_string ("Second contract"))); cls->orders[2].pay_deadline = GNUNET_TIME_UNIT_ZERO_ABS; GNUNET_assert (0 == json_object_set_new ( cls->orders[2].contract, "pay_deadline", GNUNET_JSON_from_time_abs (cls->orders[2].pay_deadline))); } /** * 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]); free_order_data (&cls->orders[2]); } /** * 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 }; uint64_t serial; /* 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 */ { struct GNUNET_HashCode unused; if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->lookup_order (plugin->cls, cls->instance.instance.id, cls->orders[1].id, NULL, &unused, 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)); serial = get_order_serial (&cls->instance, &cls->orders[0]); 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 update contract terms */ json_object_set_new (cls->orders[0].contract, "some_new_field", json_string ("another value")); TEST_RET_ON_FAIL (test_update_contract_terms (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_contract_terms (&cls->instance, &cls->orders[0])); /* Test lookup order status */ TEST_RET_ON_FAIL (test_lookup_order_status (&cls->instance, &cls->orders[0], false)); { struct GNUNET_HashCode h_contract_terms; bool order_paid = false; 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 lookup payment status */ TEST_RET_ON_FAIL (test_lookup_payment_status (serial, NULL, false, false)); { bool paid; bool wired; TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->lookup_payment_status (plugin->cls, 256, NULL, &paid, &wired), "Lookup payment status failed\n"); } /* 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 (serial, NULL, true, false)); TEST_RET_ON_FAIL (test_lookup_payment_status (serial, "test_orders_session", true, false)); { bool paid; bool wired; TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == plugin->lookup_payment_status (plugin->cls, serial, "bad_session", &paid, &wired), "Lookup payment status failed\n"); } /* Test lookup order by fulfillment */ TEST_RET_ON_FAIL (test_lookup_order_by_fulfillment (&cls->instance, &cls->orders[0], "test_orders_session")); { char *order_id; if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->lookup_order_by_fulfillment (plugin->cls, cls->instance.instance.id, "fulfillment_url", "test_orders_session", &order_id)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup order by fulfillment failed\n"); GNUNET_free (order_id); return 1; } } /* 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 (serial, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_payment_status (serial, NULL, true, true)); TEST_RET_ON_FAIL (test_mark_order_wired (1007, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* If an order has been claimed and we aren't past the pay deadline, we can't delete it. */ TEST_RET_ON_FAIL (test_delete_order (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test we can't delete before the legal expiration */ TEST_RET_ON_FAIL (test_delete_contract_terms (&cls->instance, &cls->orders[0], GNUNET_TIME_UNIT_MONTHS, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test deleting contract terms */ TEST_RET_ON_FAIL (test_delete_contract_terms (&cls->instance, &cls->orders[0], GNUNET_TIME_UNIT_ZERO, 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_TIME_UNIT_ZERO, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test delete order where we aren't past the deadline, but the order is unclaimed. */ 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)); /* Test we can also delete a claimed order that's past the pay deadline. */ TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[2], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->orders[2], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_delete_order (&cls->instance, &cls->orders[2], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); return 0; } /** * Does all tasks for testing orders. * * @return 0 when successful, 1 otherwise. */ static int test_orders (void) { 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 ********** */ /** * A container for exchange signing key data. */ struct ExchangeSignkeyData { /** * The master private key of the exchange. */ struct TALER_MasterPrivateKeyP master_priv; /** * The master public key of the exchange. */ struct TALER_MasterPublicKeyP master_pub; /** * A signature made with the master keys. */ struct TALER_MasterSignatureP master_sig; /** * The private key of the exchange. */ struct TALER_ExchangePrivateKeyP exchange_priv; /** * The public key of the exchange. */ struct TALER_ExchangePublicKeyP exchange_pub; /** * When the signing key becomes valid. */ struct GNUNET_TIME_Absolute start_date; /** * When the signing key stops being used. */ struct GNUNET_TIME_Absolute expire_date; /** * When the signing key becomes invalid for proof. */ struct GNUNET_TIME_Absolute end_date; }; /** * Creates an exchange signing key. * * @param signkey the signing key data to fill. */ 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 { /** * When the deposit was made. */ struct GNUNET_TIME_Absolute timestamp; /** * Hash of the associated order's contract terms. */ struct GNUNET_HashCode h_contract_terms; /** * Public key of the coin that has been deposited. */ struct TALER_CoinSpendPublicKeyP coin_pub; /** * URL of the exchange. */ const char *exchange_url; /** * Value of the coin with fees applied. */ struct TALER_Amount amount_with_fee; /** * Fee charged for deposit. */ struct TALER_Amount deposit_fee; /** * Fee to be charged in case of a refund. */ struct TALER_Amount refund_fee; /** * Fee charged after the money is wired. */ struct TALER_Amount wire_fee; /** * Hash of the wire details. */ struct GNUNET_HashCode h_wire; /** * Signature the exchange made on this deposit. */ struct TALER_ExchangeSignatureP exchange_sig; }; /** * Generates deposit data for an order. * * @param instance the instance to make the deposit to. * @param account the merchant account to use. * @param order the order this deposit is for. * @param signkey the signing key to use. * @param deposit the deposit data to fill. */ 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_contract_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); } /** * Tests inserting an exchange signing key into the database. * * @param signkey the signing key to insert. * @param expected_result the result we expect the database to return. * @return 0 on success, 1 otherwise. */ 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; } /** * Tests inserting a deposit into the database. * * @param instance the instance the deposit was made to. * @param signkey the signing key used. * @param deposit the deposit information to insert. * @param expected_result the result we expect the database to return. * @return 0 on success, 1 otherwise. */ 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; } /** * 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; }; /** * Called after 'test_lookup_deposits'. * * @param cls pointer to the test lookup closure. * @param coin_pub public key of the coin deposited. * @param amount_with_fee amount of the deposit with fees. * @param deposit_fee fee charged for the deposit. * @param refund_fee fee charged in case of a refund. * @param wire_fee fee charged when the money is wired. */ 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; } } } /** * Tests looking up deposits from the database. * * @param instance the instance to lookup deposits from. * @param h_contract_terms the contract terms that the deposits should have. * @param deposits_length length of @e deposits. * @param deposits the deposits we expect to be found. * @return 0 on success, 1 otherwise. */ 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 (0 <= 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 (%d)\n", cmp.results_length); 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; } /** * Called after 'test_lookup_deposits_contract_and_coin'. * * @param cls pointer to the test lookup closure. * @param exchange_url URL to the exchange * @param amount_with_fee amount of the deposit with fees. * @param deposit_fee fee charged for the deposit. * @param refund_fee fee charged in case of a refund. * @param wire_fee fee charged when the money is wired. * @param h_wire hash of the wire transfer details. * @param deposit_timestamp when the deposit was made. * @param refund_deadline deadline for refunding the deposit. * @param exchange_sig signature the exchange made on the deposit. * @param exchange_pub public key of the exchange. */ 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; } } } /** * Tests lookup of deposits by contract and coin. * * @param instance the instance to lookup from. * @param h_contract the contract terms the deposits should have. * @param coin_pub the public key of the coin the deposits should have. * @param deposits_length length of @e deposits. * @param deposits the deposits the db is expected to find. * @return 0 on success, 1 otherwise. */ 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; } /** * Called after 'test_lookup_deposits_by_order'. * * @param cls pointer to the test lookup closure. * @param deposit_serial row number of the deposit in the database. * @param exchange_url URL to the exchange * @param amount_with_fee amount of the deposit with fees. * @param h_wire hash of the wire transfer details. * @param amount_with_fee amount of the deposit with fees. * @param deposit_fee fee charged for the deposit. * @param coin_pub public key of the coin deposited. */ 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; } } /** * Tests looking up deposits by associated order. * * @param order_serial row number of the order to lookup for. * @param deposits_length length of @e deposits_length. * @param deposits the deposits we expect to be found. * @return 0 on success, 1 otherwise. */ 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; } /** * Container for information for looking up the row number of a deposit. */ struct LookupDepositSerial_Closure { /** * The deposit we're looking for. */ const struct DepositData *deposit; /** * The serial found. */ uint64_t serial; }; /** * Called after 'get_deposit_serial'. * * @param cls pointer to the test lookup closure. * @param deposit_serial row number of the deposit in the database. * @param exchange_url URL to the exchange * @param amount_with_fee amount of the deposit with fees. * @param h_wire hash of the wire transfer details. * @param amount_with_fee amount of the deposit with fees. * @param deposit_fee fee charged for the deposit. * @param coin_pub public key of the coin deposited. */ static void get_deposit_serial_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 LookupDepositSerial_Closure *lookup_cls = cls; if (NULL == lookup_cls) return; if ((0 == strcmp (lookup_cls->deposit->exchange_url, exchange_url)) && (0 == GNUNET_memcmp (&lookup_cls->deposit->h_wire, h_wire)) && (GNUNET_OK == TALER_amount_cmp_currency ( &lookup_cls->deposit->amount_with_fee, amount_with_fee)) && (0 == TALER_amount_cmp (&lookup_cls->deposit->amount_with_fee, amount_with_fee)) && (GNUNET_OK == TALER_amount_cmp_currency ( &lookup_cls->deposit->deposit_fee, deposit_fee)) && (0 == TALER_amount_cmp (&lookup_cls->deposit->deposit_fee, deposit_fee)) && (0 == GNUNET_memcmp (&lookup_cls->deposit->coin_pub, coin_pub))) lookup_cls->serial = deposit_serial; } /** * Convenience function to retrieve the row number of a deposit in the database. * * @param instance the instance to get deposits from. * @param order the order associated with the deposit. * @param deposit the deposit to lookup the serial for. * @return the row number of the deposit. */ static uint64_t get_deposit_serial (const struct InstanceData *instance, const struct OrderData *order, const struct DepositData *deposit) { uint64_t order_serial = get_order_serial (instance, order); struct LookupDepositSerial_Closure lookup_cls = { .deposit = deposit, .serial = 0 }; GNUNET_assert (0 < plugin->lookup_deposits_by_order (plugin->cls, order_serial, &get_deposit_serial_cb, &lookup_cls)); GNUNET_assert (0 != lookup_cls.serial); return lookup_cls.serial; } /** * 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 orders[2]; /** * The array of deposits */ struct DepositData deposits[3]; }; /** * Initializes data for testing deposits. * * @param cls the test closure to initialize. */ 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->orders[0]); make_order ("test_deposits_od_2", &cls->orders[1]); /* Deposit */ make_deposit (&cls->instance, &cls->account, &cls->orders[0], &cls->signkey, &cls->deposits[0]); make_deposit (&cls->instance, &cls->account, &cls->orders[0], &cls->signkey, &cls->deposits[1]); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:29.00", &cls->deposits[1].amount_with_fee)); make_deposit (&cls->instance, &cls->account, &cls->orders[1], &cls->signkey, &cls->deposits[2]); } /** * Cleans up memory after testing deposits. * * @param cls the closure containing memory to free. */ static void post_test_deposits (struct TestDeposits_Closure *cls) { free_instance_data (&cls->instance); json_decref (cls->orders[0].contract); json_decref (cls->orders[1].contract); } /** * Runs tests for deposits. * * @param cls the closure containing test data. * @return 0 on success, 1 otherwise. */ 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)); TEST_RET_ON_FAIL (test_insert_exchange_signkey (&cls->signkey, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Insert an order */ TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert contract terms */ TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->orders[0], 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_RET_ON_FAIL (test_lookup_deposits (&cls->instance, &cls->deposits[2].h_contract_terms, 0, NULL)); /* 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_insert_order (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposits[2], 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_RET_ON_FAIL (test_lookup_deposits (&cls->instance, &cls->deposits[2].h_contract_terms, 1, &cls->deposits[2])); /* Test lookup deposits by order */ { uint64_t order_serial = get_order_serial (&cls->instance, &cls->orders[0]); TEST_RET_ON_FAIL (test_lookup_deposits_by_order (order_serial, 2, cls->deposits)); order_serial = get_order_serial (&cls->instance, &cls->orders[1]); TEST_RET_ON_FAIL (test_lookup_deposits_by_order (order_serial, 1, &cls->deposits[2])); } return 0; } /** * Handles functionality for testing deposits. * * @return 0 on success, 1 otherwise. */ static int test_deposits (void) { 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 ********** */ /** * Container for wire fee data for an exchange. */ struct WireFeeData { /** * The method used. */ const char *wire_method; /** * Hash of the wire method. */ struct GNUNET_HashCode h_wire_method; /** * Wire fee charged. */ struct TALER_Amount wire_fee; /** * Closing fee charged. */ struct TALER_Amount closing_fee; /** * Start date of the wire fee. */ struct GNUNET_TIME_Absolute wire_fee_start; /** * End date of the wire fee. */ struct GNUNET_TIME_Absolute wire_fee_end; /** * Signature on the wire fee. */ struct TALER_MasterSignatureP fee_sig; }; /** * Creates data for an exchange wire fee. * * @param signkey the exchange signing key data. * @param wire_fee where to store the wire fee data. */ 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); } /** * Container for wire transfer data. */ struct TransferData { /** * Id of the transfer. */ struct TALER_WireTransferIdentifierRawP wtid; /** * The main data for the transfer. */ struct TALER_EXCHANGE_TransferData data; /** * URL to the exchange the transfer was made through. */ const char *exchange_url; /** * How much the fee for the deposit was. */ struct TALER_Amount deposit_fee; /** * Whether the transfer has been confirmed. */ bool confirmed; /** * Whether the transfer has been verified. */ bool verified; }; /** * Creates a transfer for use with testing. * * @param deposits_length length of @e deposits. * @param deposits list of deposits to combine into one transfer. * @param transfer where to write the transfer data. */ static void make_transfer (const struct ExchangeSignkeyData *signkey, unsigned int deposits_length, const struct DepositData deposits[], struct TransferData *transfer) { GNUNET_CRYPTO_seed_weak_random (585); GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, &transfer->wtid, sizeof (struct TALER_WireTransferIdentifierRawP)); transfer->exchange_url = deposits[0].exchange_url; transfer->verified = false; transfer->confirmed = false; struct TALER_TrackTransferDetails *details = NULL; transfer->data.details_length = 0; GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (deposits[0].amount_with_fee.currency, &transfer->data.total_amount)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (deposits[0].amount_with_fee.currency, &transfer->deposit_fee)); for (unsigned int i = 0; i < deposits_length; ++i) { GNUNET_array_grow (details, transfer->data.details_length, i + 1); details[i].h_contract_terms = deposits[i].h_contract_terms; details[i].coin_pub = deposits[i].coin_pub; details[i].coin_value = deposits[i].amount_with_fee; details[i].coin_fee = deposits[i].deposit_fee; GNUNET_assert (0 <= TALER_amount_add (&transfer->data.total_amount, &transfer->data.total_amount, &deposits[i].amount_with_fee)); GNUNET_assert (0 <= TALER_amount_add (&transfer->deposit_fee, &transfer->deposit_fee, &deposits[i].deposit_fee)); } transfer->data.exchange_pub = signkey->exchange_pub; transfer->data.execution_time = GNUNET_TIME_absolute_get (); transfer->data.details = details; GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:0.50", &transfer->data.wire_fee)); } /** * Tests looking up a transfer from the database. * * @param exchange_url url to the exchange of the transfer. * @param wtid id of the transfer. * @param total_expected the total amount of the transfer. * @param fee_expected the fee on the transfer. * @param time_expected when the transfer was made. * @param verified_expected whether the transfer was verified. * @return 1 on success, 0 otherwise. */ static int test_lookup_transfer (const struct TransferData *transfer) { struct TALER_Amount total_with_fee; struct TALER_Amount total; struct TALER_Amount fee; struct GNUNET_TIME_Absolute time; bool verified; if (1 != plugin->lookup_transfer (plugin->cls, transfer->exchange_url, &transfer->wtid, &total, &fee, &time, &verified)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer failed\n"); return 1; } GNUNET_assert (0 <= TALER_amount_add (&total_with_fee, &transfer->data.total_amount, &transfer->data.wire_fee)); if ((GNUNET_OK != TALER_amount_cmp_currency (&total_with_fee, &total)) || (0 != TALER_amount_cmp (&total_with_fee, &total)) || (GNUNET_OK != TALER_amount_cmp_currency (&transfer->data.wire_fee, &fee)) || (0 != TALER_amount_cmp (&transfer->data.wire_fee, &fee)) || (transfer->data.execution_time.abs_value_us != time.abs_value_us) || (transfer->verified != verified)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup transfer failed: mismatched data\n"); return 1; } return 0; } /** * Closure for testing 'lookup_transfer_summary' */ struct TestLookupTransferSummary_Closure { /** * Id of the order the transfer was made for. */ const char *order_id; /** * The value of the deposit made. */ const struct TALER_Amount *deposit_value; /** * The fee on the deposit made. */ const struct TALER_Amount *deposit_fee; /** * 0 if the comparison is true, 1 if false. */ int result; }; /** * Called after 'test_lookup_transfer_summary'. * * @param cls pointer to 'TestLookupTransferSummary_Closure'. * @param order_id id of the order the transfer was made for. * @param deposit_value the value of the deposit made. * @param deposit_fee the fee on the deposit made. */ 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; } /** * Tests looking up a transfer's summary. * * @param exchange_url url to the exchange for the transfer. * @param wtid identifier of the transfer. * @param expected_order_id the id of the order associated with the transfer. * @param expected_deposit_value the amount of the deposit made for the transfer. * @param expected_deposit_fee the fee on the deposit made for the transfer. * @return 1 on success, 0 otherwise. */ 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; } /** * Closure for testing 'lookup_transfer_details'. */ struct TestLookupTransferDetails_Closure { /** * Length of @e details_to_cmp. */ unsigned int details_to_cmp_length; /** * The details we expect to find. */ const struct TALER_TrackTransferDetails *details_to_cmp; /** * Number of results matching each detail in @e details_to_cmp. */ unsigned int *results_matching; /** * Total number of results found. */ unsigned int results_length; }; /** * Called after 'test_lookup_transfer_details'. * * @param cls pointer to 'TestLookupTransferDetails_Closure'. * @param current_offset offset within transfer details. * @param details the details that were found. */ 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; } /** * Tests looking up details for a wire transfer. * * @param exchange_url url to the exchange. * @param wtid id of the transfer. * @param details_length the length of @e details. * @param details the details we expect to be returned. * @return 1 on success, 0 otherwise. */ 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; } /** * Closure for 'lookup_transfer_details_by_order'. */ struct TestLookupTransferDetailsByOrder_Closure { /** * Length of @e transfers_to_cmp. */ unsigned int transfers_to_cmp_length; /** * List of transfers that we expect to find. */ const struct TransferData *transfers_to_cmp; /** * How many results match the corresponding element of @e transfers_to_cmp. */ unsigned int *results_matching; /** * Total number of results found. */ unsigned int results_length; }; /** * Called after 'test_lookup_transfer_details_by_order'. * * @param cls pointer to 'TestLookupTransferDetailsByOrder_Closure'. * @param wtid identifier of the transfer found. * @param exchange_url exchange url of the transfer found. * @param execution_time when the transfer found occurred. * @param deposit_value amount of the deposit for the transfer found. * @param deposit_fee amount of the fee for the deposit of the transfer. * @param transfer_confirmed whether the transfer was confirmed. */ 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].data.total_amount, deposit_value)) && (0 == TALER_amount_cmp (&cmp->transfers_to_cmp[i].data.total_amount, 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; } } /** * Tests looking up wire transfers associated with an order. * * @param order_serial the order to be queried. * @param transfers_length length of @e transfers. * @param transfers the transfers we expect to be found. * @return 0 on success, 1 otherwise. */ static int test_lookup_transfer_details_by_order (uint64_t order_serial, unsigned int transfers_length, const struct TransferData *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; } /** * Tests inserting wire fee data for an exchange. * * @param signkey the signing key for the exchange. * @param wire_fee the wire fee data. * @param expected_result what the database should return. * @return 0 on success, 1 otherwise. */ 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; } /** * Tests looking up wire fee data for an exchange. * * @param signkey the signing key to use for lookup. * @param wire_fee_data the wire fee data we expect to find. * @return 0 on success, 1 otherwise. */ 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; } /** * Closure for 'lookup_transfers'. */ struct TestLookupTransfers_Closure { /** * Length of @e transfers_to_cmp. */ unsigned int transfers_to_cmp_length; /** * The transfers we expect to find. */ const struct TransferData *transfers_to_cmp; /** * Number of results matching each transfer. */ unsigned int *results_matching; /** * Total number of results found. */ unsigned int results_length; }; /** * Function called after 'test_lookup_transfers'. * * @param cls pointer to 'TestLookupTransfers_Closure'. * @param credit_amount how much was wired to the merchant (minus fees) * @param wtid wire transfer identifier * @param payto_uri target account that received the wire transfer * @param exchange_url base URL of the exchange that made the wire transfer * @param transfer_serial_id serial number identifying the transfer in the backend * @param execution_time when did the exchange make the transfer, #GNUNET_TIME_UNIT_FOREVER_ABS * if it did not yet happen * @param verified true if we checked the exchange's answer and liked it, * false there is a problem (verification failed or did not yet happen) * @param confirmed true if the merchant confirmed this wire transfer * false if it is so far only claimed to have been made by the exchange */ 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].data.total_amount, credit_amount)) && (0 == TALER_amount_cmp (&cmp->transfers_to_cmp[i].data.total_amount, credit_amount)) && (cmp->transfers_to_cmp[i].data.execution_time.abs_value_us == execution_time.abs_value_us)) { cmp->results_matching[i] += 1; } } cmp->results_length += 1; } /** * Tests looking up transfers from the database. * * @param instance the instance to lookup from. * @param account the account the transfer was made to. * @param before do not return transfers before this time. * @param after do not return transfers after this time. * @param limit maximum number of transfers to return. * @param offset row in the database to start with. * @param filter_verified how to filter verified transfers. * @param transfers_length length of @e transfers. * @param transfers the transfers we expect to find. * @return 0 on success, 1 otherwise. */ static int test_lookup_transfers (const struct InstanceData *instance, const struct TALER_MERCHANTDB_AccountDetails *account, 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 TransferData *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->instance.id, account->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; } /** * Tests inserting a transfer into the database. * * @param instance the instance to use. * @param account the account to transfer to. * @param transfer the transfer to insert. * @param expected_result the result we expect the db to return. * @return 0 on success, 1 otherwise. */ static int test_insert_transfer (const struct InstanceData *instance, const struct TALER_MERCHANTDB_AccountDetails *account, const struct TransferData *transfer, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_transfer (plugin->cls, instance->instance.id, transfer->exchange_url, &transfer->wtid, &transfer->data.total_amount, account->payto_uri, transfer->confirmed), "Insert transfer failed\n"); return 0; } /** * Tests linking a deposit to a transfer. * * @param instance the instance that the deposit and transfer are for. * @param signkey the signing used on the deposit. * @param order the order the deposit and transfer were made for. * @param transfer the transfer. * @param expected_result the result the database should return. * @return 0 on success, 1 otherwise. */ static int test_insert_deposit_to_transfer (const struct InstanceData *instance, const struct ExchangeSignkeyData *signkey, const struct OrderData *order, const struct DepositData *deposit, const struct TransferData *transfer, enum GNUNET_DB_QueryStatus expected_result) { const struct TALER_EXCHANGE_DepositData deposit_data = { .exchange_pub = signkey->exchange_pub, .exchange_sig = deposit->exchange_sig, .wtid = transfer->wtid, .execution_time = transfer->data.execution_time, .coin_contribution = deposit->amount_with_fee }; uint64_t deposit_serial = get_deposit_serial (instance, order, deposit); TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_deposit_to_transfer (plugin->cls, deposit_serial, &deposit_data), "insert deposit to transfer failed\n"); return 0; } /** * Inserts details for a transfer into the database. * * @param instance the instance the transfer is in. * @param account the destination account for the transfer. * @param transfer the transfer we are adding details to. * @param expected_result the result expected from the db. * @return 0 on success, 1 otherwise. */ static int test_insert_transfer_details (const struct InstanceData *instance, const struct TALER_MERCHANTDB_AccountDetails *account, const struct TransferData *transfer, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_transfer_details (plugin->cls, instance->instance.id, transfer->exchange_url, account->payto_uri, &transfer->wtid, &transfer->data), "Insert transfer details failed\n"); return 0; } /** * Container for data used when testing transfers. */ struct TestTransfers_Closure { /** * The instance. */ 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[2]; /** * The transfers. */ struct TransferData transfers[1]; }; /** * Prepares for testing transfers. * * @param cls the test data. */ 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[0]); make_wire_fee (&cls->signkey, &cls->wire_fee[1]); cls->wire_fee[1].wire_method = "wire-method-2"; GNUNET_CRYPTO_hash (cls->wire_fee[1].wire_method, strlen (cls->wire_fee[1].wire_method) + 1, &cls->wire_fee[1].h_wire_method); /* Transfers */ make_transfer (&cls->signkey, 1, &cls->deposit, &cls->transfers[0]); cls->transfers[0].confirmed = true; } /** * Cleans up after testing transfers. * * @param cls the test data. */ static void post_test_transfers (struct TestTransfers_Closure *cls) { free_instance_data (&cls->instance); free_order_data (&cls->order); } /** * Runs the tests for transfers. * * @param cls the test data. * @return 0 on success, 1 otherwise. */ static int run_test_transfers (struct TestTransfers_Closure *cls) { uint64_t order_serial; /* 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[0].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[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double insertion fails */ TEST_RET_ON_FAIL (test_insert_wire_fee (&cls->signkey, &cls->wire_fee[0], 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[0])); /* Test different wire fees for different methods. */ TEST_RET_ON_FAIL (test_insert_wire_fee (&cls->signkey, &cls->wire_fee[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_wire_fee (&cls->signkey, &cls->wire_fee[1])); /* 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)); order_serial = get_order_serial (&cls->instance, &cls->order); /* Insert the deposit */ TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposit, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert the transfer */ TEST_RET_ON_FAIL (test_insert_transfer (&cls->instance, &cls->account, &cls->transfers[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_transfer (&cls->instance, &cls->account, &cls->transfers[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); TEST_RET_ON_FAIL (test_insert_deposit_to_transfer (&cls->instance, &cls->signkey, &cls->order, &cls->deposit, &cls->transfers[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_deposit_to_transfer (&cls->instance, &cls->signkey, &cls->order, &cls->deposit, &cls->transfers[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); TEST_RET_ON_FAIL (test_insert_transfer_details (&cls->instance, &cls->account, &cls->transfers[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_payment_status (order_serial, NULL, false, true)); TEST_RET_ON_FAIL (test_lookup_transfer (&cls->transfers[0])); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->set_transfer_status_to_verified (plugin->cls, cls->deposit. exchange_url, &cls-> transfers[0]. wtid), "Set transfer status to verified failed\n"); cls->transfers[0].verified = true; TEST_RET_ON_FAIL (test_lookup_transfer (&cls->transfers[0])); TEST_RET_ON_FAIL (test_lookup_transfer_summary (cls->deposit.exchange_url, &cls->transfers[0].wtid, cls->order.id, &cls->deposit.amount_with_fee, &cls->deposit.deposit_fee)); TEST_RET_ON_FAIL (test_lookup_transfer_details (cls->deposit.exchange_url, &cls->transfers[0].wtid, 1, &cls->transfers[0].data. details[0])); TEST_RET_ON_FAIL (test_lookup_transfer_details_by_order (order_serial, 1, &cls->transfers[0])); TEST_RET_ON_FAIL (test_lookup_transfers (&cls->instance, &cls->account, GNUNET_TIME_UNIT_FOREVER_ABS, GNUNET_TIME_UNIT_ZERO_ABS, 8, 0, TALER_EXCHANGE_YNA_ALL, 1, &cls->transfers[0])); return 0; } /** * Takes care of all work for testing transfers. * * @return 0 on success, 1 otherwise. */ static int test_transfers (void) { 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; }; /** * Tests inserting a reserve into the database. * @paper instance the instance the reserve is for. * @param reserve the reserve to insert. * @param expected_result the result we expect to receive from the db. * * @return 0 on success, 1 otherwise. */ 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; } /** * Container for looking up reserves. */ struct TestLookupReserve_Closure { /** * The reserve we expect to find. */ const struct ReserveData *reserve_to_cmp; /** * The length of @e tips. */ unsigned int tips_length; /** * The tips that have been authorized from the reserve. */ const struct TALER_MERCHANTDB_TipDetails *tips; /** * 1 if the result matches, 0 otherwise. */ int result_matches; }; /** * Called after test_lookup_reserve. * @param cls a pointer to TestLookupReserve_Closure. * @param creation_time time when the reserve was setup * @param expiration_time time when the reserve will be closed by the exchange * @param merchant_initial_amount initial amount that the merchant claims to have filled the * reserve with * @param exchange_initial_amount initial amount that the exchange claims to have received * @param picked_up_amount total of tips that were picked up from this reserve * @param committed_amount total of tips that the merchant committed to, but that were not * picked up yet * @param active true if the reserve is still active (we have the private key) * @param tips_length length of the @a tips array * @param tips information about the tips created by this reserve * * @return 0 on success, 1 otherwise. */ 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; } /** * Tests looking up details of a reserve from the database. * @param instance the instance to lookup the reserve from. * @param reserve_pub the public key of the reserve we are looking for. * @param reserve the data we expect to find. * * @return 0 on success, 1 otherwise. */ static int test_lookup_reserve (const struct InstanceData *instance, 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->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; } /** * Container for looking up multiple reserves. */ struct TestLookupReserves_Closure { /** * The length of @e reserves_to_cmp. */ unsigned int reserves_to_cmp_length; /** * The reserves we expect to find from the lookup. */ const struct ReserveData *reserves_to_cmp; /** * The number of results matching each reserve we were looking for. */ unsigned int *results_matching; /** * The total number of results found from the lookup. */ unsigned int results_length; }; /** * Called after test_lookup_reserves. * @param cls pointer to a TestLookupReserves_Closure. * @param reserve_pub public key of the reserve * @param creation_time time when the reserve was setup * @param expiration_time time when the reserve will be closed by the exchange * @param merchant_initial_amount initial amount that the merchant claims to have filled the * reserve with * @param exchange_initial_amount initial amount that the exchange claims to have received * @param pickup_amount total of tips that were picked up from this reserve * @param committed_amount total of tips that the merchant committed to, but that were not * picked up yet * @param active true if the reserve is still active (we have the private key) */ 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; } /** * Test looking up reserves for an instance. * @param instance the instance to get the reserves from. * * @return 0 on success, 1 otherwise. */ static int test_lookup_reserves (const struct InstanceData *instance, 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->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; } /** * Called after test_lookup_pending_reserves. * @param cls pointer to a TestLookupReserves_Closure. * @param instance_id the id of the instance the reserve belongs to. * @param exchange_url url of the exchange for this reserve. * @param reserve_pub public key of this reserve. * @param expected_amount what the amount in the reserve is, according to the db. */ 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; } /** * Tests looking up reserves that are not activated from the database. * @param reserves_length length of @e reserves. * @param reserves the reserves that the db is expected to return. * * @return 0 on success, 1 otherwise. */ 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; } /** * Container for all tip data relevant to the database. */ struct TipData { /** * The details of the tip. */ struct TALER_MERCHANTDB_TipDetails details; /** * Where the user should be redirected. */ const char *next_url; /** * When the tip expires. */ struct GNUNET_TIME_Absolute expiration; }; /** * Creates a tip for testing. * @param tip the tip to fill with data. */ 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"; } /** * Tests authorizing a tip. * @param instance the instance authorizing the tip. * @param reserve where the tip is coming from. * @param tip the tip to authorize. * * @return 0 on success, 1 otherwise. */ 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; } /** * Tests looking up a tip from the database. * @param instance the instance to look up tips from. * @param reserve the reserve to look up tips from. * @param tip the tip we expect to find (uses @e tip_id to perform lookup). * @param expected_total_picked_up how much of the tip should have been * picked up. * * @return 0 on success, 1 otherwise. */ 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"); 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"); GNUNET_free (exchange_url); return 1; } GNUNET_free (exchange_url); return 0; } /** * Tests looking up the details of a tip from the database. * @param instance the instance the tip is in. * @param reserve the reserve the tip was authorized from. * @param tip the tip we expect to find (uses @e tip_id to perform lookup). * @param expected_total_picked_up how much of the tip should have been * picked up. * @param expected_pickups_length the length of @e expected_pickups. * @param expected_pickups the pickups that we expect to be associated with * the tip. * * @return 0 on success, 1 otherwise. */ 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 (0 > 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"); GNUNET_free (justification); 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"); GNUNET_free (justification); 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"); GNUNET_free (justification); GNUNET_free (pickups); return 1; } } return 0; } /** * Utility function for freeing an array of RSA signatures. * @param sigs_length length of @e sigs. * @param sigs the signatures to free. */ static void free_rsa_signature_array (unsigned int sigs_length, struct GNUNET_CRYPTO_RsaSignature **sigs) { for (unsigned int i = 0; sigs_length > i; ++i) GNUNET_free (sigs[i]); } /** * Tests looking up a tip pickup. * @param instance the instance to look up from. * @param tip the tip the pickup was made for. * @param pickup_id id of the pickup to look up. * @param expected_exchange_url exchange url for the pickup. * @param expected_reserve_priv reserve private key for the pickup. * @param expected_sigs_length length of @e expected_sigs. * @param expected_sigs the signatures we expect to be made for the pickup. * * @return 0 on success, 1 otherwise. */ static int test_lookup_pickup (const struct InstanceData *instance, const struct TipData *tip, 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->instance.id, &tip->details.tip_id, pickup_id, &exchange_url, &reserve_priv, expected_sigs_length, sigs)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pickup failed\n"); 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"); 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"); GNUNET_free (exchange_url); free_rsa_signature_array (expected_sigs_length, sigs); return 1; } } return 0; } /** * Closure for testing lookup_tips. */ struct TestLookupTips_Closure { /** * The length of @e tips_to_cmp. */ unsigned int tips_to_cmp_length; /** * The tips that we are expecting to find. */ const struct TipData *tips_to_cmp; /** * The number of results found from the lookup. */ unsigned int results_length; /** * Whether each result matches with the corresponding tip in @tips_to_cmp. */ bool *results_match; }; /** * Called after test_lookup_tips. * @param cls pointer to a TestLookupTips_Closure. * @param row_id the row id of the tip. * @param tip_id the id of the tip. * @param amount the amount of the tip. */ 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; } } /** * Tests looking up the tips from the database. * @param instance the instance to look up tips from. * @param expired how to filter expired tips. * @param offset where to start retrieving tips. * @param tips_length length of @e tips. * @param tips the tips that we expect to find. * * @return 0 on success, 1 otherwise. */ 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 * @param tips_length length of @e tips. * @param tips the array of tips to reverse. */ 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]; } /** * Container for data for testing tips. */ struct TestTips_Closure { /** * The instance. */ struct InstanceData instance; /** * The tip reserve data. */ struct ReserveData reserve; /** * Reserve data that is expired. */ struct ReserveData expired_reserve; /** * A normal tip. */ struct TipData tip; /** * A tip that is too large to authorize. */ struct TipData bigtip; /** * Array of tips for testing lookups. */ struct TipData tips[5]; /** * Id of a pickup. */ struct GNUNET_HashCode pickup_id; /** * Private key of the pickup. */ struct GNUNET_CRYPTO_RsaPrivateKey *pickup_priv; /** * Signature for the pickup. */ struct GNUNET_CRYPTO_RsaSignature *pickup_sig; }; /** * Prepares for testing tips functionality. * @param cls the data to prepare. */ 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); } /** * Cleans up after testing tips. * @param cls the data to clean up. */ 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); } /** * Runs tests for tips. * @param cls container of test data. * * @return 0 on success, 1 on failure. */ static int run_test_tips (struct TestTips_Closure *cls) { struct TALER_Amount zero; GNUNET_assert (GNUNET_OK == 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, &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, &cls->tip, &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, 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; } /** * Handles all logic for testing tips in the database. * * @return 0 on success, 1 on failure. */ static int test_tips (void) { 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; } /** * Closure for testing lookup_refunds. */ struct TestLookupRefunds_Closure { /** * Length of @e coin_pub_to_cmp and @e refund_amount_to_cmp. */ unsigned int refunds_to_cmp_length; /** * Public keys of the refunded coins. */ const struct TALER_CoinSpendPublicKeyP *coin_pub_to_cmp; /** * Amount of each refund. */ const struct TALER_Amount *refund_amount_to_cmp; /** * Number of results matching each refund provided. */ unsigned int *results_matching; /** * Total number of results returned; */ unsigned int results_length; }; /** * Called after test_lookup_refunds. * @param cls pointer to a TestLookupRefunds_Closure. * @param coin_pub the public key of the coin for the refund found. * @param refund_amount the amount of the refund found. */ 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; } } } /** * Tests looking up refunds from the database. * @param instance the instance to look up refunds for. * @param h_contract_terms hash of the contract terms the refunds are for. * @param refunds_length length of @e coin_pubs and @e refund_amounts. * @param coin_pubs the public keys of the coins that were refunded. * @param refund_amounts the amounts of the coins that were refunded. * * @return 0 on success, 1 otherwise. */ static int test_lookup_refunds (const struct InstanceData *instance, 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->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; } /** * Container for refund data. */ struct RefundData { /** * When the refund occurred. */ struct GNUNET_TIME_Absolute timestamp; /** * Reason for the refund. */ const char *reason; /** * Amount of the refund. */ struct TALER_Amount refund_amount; /** * Public key of the coin that was refunded. */ const struct TALER_CoinSpendPublicKeyP *coin_pub; /** * URL to the exchange that did the refund. */ const char *exchange_url; }; /** * Creates a refund for testing with. * @param deposit the deposit being refunded. * @param refund the data to set. */ 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; } /** * Container for proof of a refund. */ struct RefundProofData { /** * Fee charged for the refund. */ struct TALER_Amount refund_fee; /** * The exchange's signature on the refund. */ struct TALER_ExchangeSignatureP exchange_sig; }; /** * Closure for testing lookup_refunds_detailed. */ struct TestLookupRefundsDetailed_Closure { /** * Length of @e refunds_to_cmp. */ unsigned int refunds_to_cmp_length; /** * The refunds we expect to find. */ const struct RefundData *refunds_to_cmp; /** * Whether to compare the timestamps or not (if we don't have direct control * of the timestamps, there will be differences on the order of microseconds * that can be ignored). */ bool cmp_timestamps; /** * The number of results matching each refund. */ unsigned int *results_matching; /** * The total number of results from the db. */ unsigned int results_length; }; /** * Called after test_lookup_refunds_detailed. * @param cls pointer to a TestLookupRefundsDetailed_Closure. * @param refund_serial unique serial number of the refund * @param timestamp time of the refund (for grouping of refunds in the wallet UI) * @param coin_pub public coin from which the refund comes from * @param exchange_url URL of the exchange that issued @a coin_pub * @param rtransaction_id identificator of the refund * @param reason human-readable explanation of the refund * @param refund_amount refund amount which is being taken from @a coin_pub * @param pending true if this refund has not been processed by the wallet/exchange */ 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, bool pending) { 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) || ! cmp->cmp_timestamps) && (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; } } } /** * Tests looking up refunds with details from the database. * @param instance the instance to lookup from. * @param h_contract_terms the contract terms the refunds were made for. * @param cmp_timestamps whether to compare timestamps or not. * @param refunds_length length of @e refunds. * @param refunds the refunds we expect to be returned. * * @return 0 on success, 1 otherwise. */ static int test_lookup_refunds_detailed (const struct InstanceData *instance, const struct GNUNET_HashCode *h_contract_terms, bool cmp_timestamps, 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, .cmp_timestamps = cmp_timestamps, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * refunds_length); if (0 > plugin->lookup_refunds_detailed (plugin->cls, instance->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; } /** * Closure for get_refund_serial. */ struct LookupRefundSerial_Closure { /** * The refund we are looking up the id for. */ const struct RefundData *refund; /** * The row number found. */ uint64_t serial; }; /** * Called after get_refund_serial. * @param cls pointer to a LookupRefundSerial_Closure. * @param refund_serial unique serial number of the refund * @param timestamp time of the refund (for grouping of refunds in the wallet UI) * @param coin_pub public coin from which the refund comes from * @param exchange_url URL of the exchange that issued @a coin_pub * @param rtransaction_id identificator of the refund * @param reason human-readable explanation of the refund * @param refund_amount refund amount which is being taken from @a coin_pub * @param pending true if this refund has not been processed by the wallet/exchange */ static void get_refund_serial_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, bool pending) { struct LookupRefundSerial_Closure *lookup_cls = cls; if (NULL == lookup_cls) return; if ((lookup_cls->refund->timestamp.abs_value_us == timestamp.abs_value_us) && (0 == GNUNET_memcmp (lookup_cls->refund->coin_pub, coin_pub)) && (0 == strcmp (lookup_cls->refund->exchange_url, exchange_url)) && (0 == strcmp (lookup_cls->refund->reason, reason)) && (GNUNET_OK == TALER_amount_cmp_currency ( &lookup_cls->refund->refund_amount, refund_amount)) && (0 == TALER_amount_cmp (&lookup_cls->refund->refund_amount, refund_amount))) lookup_cls->serial = refund_serial; } /** * Utility function for getting the database row number of a refund. * @param instance the instance associated with the refund. * @param h_contract_terms the contract terms the refund was made with. * @param refund the refund we are querying the row number of. * * @return the row number of the refund. */ static uint64_t get_refund_serial (const struct InstanceData *instance, const struct GNUNET_HashCode *h_contract_terms, const struct RefundData *refund) { struct LookupRefundSerial_Closure lookup_cls = { .refund = refund, .serial = 0 }; GNUNET_assert (0 < plugin->lookup_refunds_detailed (plugin->cls, instance->instance.id, h_contract_terms, &get_refund_serial_cb, &lookup_cls)); GNUNET_assert (0 != lookup_cls.serial); return lookup_cls.serial; } /** * Tests looking up proof of a refund. * @param refund_serial the row number of the refund. * @param expected_exchange_sig the exchange signature we are expecting. * @param expected_exchange_pub the exchange public key we are expecting. * * @return 0 on success, 1 otherwise. */ 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; } /** * Closure for testing refund functionality. */ struct TestRefunds_Closure { /** * The instance. */ struct InstanceData instance; /** * The merchant account. */ struct TALER_MERCHANTDB_AccountDetails account; /** * The exchange signing key. */ struct ExchangeSignkeyData signkey; /** * The order data. */ struct OrderData orders[2]; /** * The deposit data. */ struct DepositData deposits[3]; /** * The refund data. */ struct RefundData refunds[3]; /** * The refund proof data. */ struct RefundProofData refund_proof; }; /** * Prepares for testing refunds. * @param cls the closure to initialize. */ 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->orders[0]); make_order ("test_refunds_od_1", &cls->orders[1]); /* Deposit */ make_deposit (&cls->instance, &cls->account, &cls->orders[0], &cls->signkey, &cls->deposits[0]); make_deposit (&cls->instance, &cls->account, &cls->orders[0], &cls->signkey, &cls->deposits[1]); make_deposit (&cls->instance, &cls->account, &cls->orders[1], &cls->signkey, &cls->deposits[2]); /* Refund */ make_refund (&cls->deposits[0], &cls->refunds[0]); make_refund (&cls->deposits[2], &cls->refunds[1]); make_refund (&cls->deposits[2], &cls->refunds[2]); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:10.00", &cls->refunds[1].refund_amount)); cls->refunds[1].reason = "refund 1"; GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:10.00", &cls->refunds[2].refund_amount)); cls->refunds[2].reason = "refund 2"; /* 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); } /** * Cleans up after testing refunds. * @param cls the closure. */ static void post_test_refunds (struct TestRefunds_Closure *cls) { free_instance_data (&cls->instance); free_order_data (&cls->orders[0]); free_order_data (&cls->orders[1]); } /** * Runs the refund tests. * @param cls the closure. * * @return 0 on success, 1 otherwise. */ static int run_test_refunds (struct TestRefunds_Closure *cls) { struct TALER_Amount inc; uint64_t refund_serial; /* 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->orders[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Insert contract terms */ TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->orders[0], 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->orders[0], 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->refunds[0].timestamp, cls->refunds[0].coin_pub, cls->refunds[0].reason), "Refund coin failed\n"); refund_serial = get_refund_serial (&cls->instance, &cls->deposits[0].h_contract_terms, &cls->refunds[0]); /* 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->refunds[0].timestamp, cls->refunds[0].coin_pub, cls->refunds[0].reason), "Refund coin failed\n"); /* Test lookup refunds */ TEST_RET_ON_FAIL (test_lookup_refunds (&cls->instance, &cls->deposits[0].h_contract_terms, 1, cls->refunds[0].coin_pub, &cls->refunds[0].refund_amount)); /* Test lookup refunds detailed */ TEST_RET_ON_FAIL (test_lookup_refunds_detailed (&cls->instance, &cls->deposits[0]. h_contract_terms, true, 1, &cls->refunds[0])); /* Test insert refund proof */ TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->insert_refund_proof (plugin->cls, refund_serial, &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, refund_serial, &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 (TALER_MERCHANTDB_RS_TOO_HIGH == plugin->increase_refund (plugin->cls, cls->instance.instance.id, cls->orders[0].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 (TALER_MERCHANTDB_RS_SUCCESS == plugin->increase_refund (plugin->cls, cls->instance.instance.id, cls->orders[0].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)); TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance, &cls->signkey, &cls->deposits[2], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_mark_contract_paid (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test refunding a small amount of the coin, then increasing it */ GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:10.00", &inc)); TEST_COND_RET_ON_FAIL (TALER_MERCHANTDB_RS_SUCCESS == plugin->increase_refund (plugin->cls, cls->instance.instance.id, cls->orders[1].id, &inc, cls->refunds[1].reason), "Increase refund failed\n"); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:20.00", &inc)); TEST_COND_RET_ON_FAIL (TALER_MERCHANTDB_RS_SUCCESS == plugin->increase_refund (plugin->cls, cls->instance.instance.id, cls->orders[1].id, &inc, cls->refunds[2].reason), "Increase refund failed\n"); TEST_RET_ON_FAIL (test_lookup_refunds_detailed (&cls->instance, &cls->deposits[2]. h_contract_terms, false, 2, &cls->refunds[1])); return 0; } /** * All logic for testing refunds. * * @return 0 on success, 1 otherwise. */ static int test_refunds (void) { 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 that reverses an array of orders. * @param orders_length length of @e orders. * @param orders the array to reverse. */ 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]; } /** * Closure for testing all the filters for looking up orders. */ struct TestLookupOrdersAllFilters_Closure { /** * The instance. */ struct InstanceData instance; /** * The merchant account. */ struct TALER_MERCHANTDB_AccountDetails account; /** * The exchange signing key. */ struct ExchangeSignkeyData signkey; /** * The array of order ids. */ char *order_ids[64]; /** * The array of orders. */ struct OrderData orders[64]; /** * The array of deposits. */ struct DepositData deposits[64]; /** * The array of refunds. */ struct RefundData refunds[64]; }; /** * Sets up for testing lookup order filters. * @param cls the closure. */ 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]); } } /** * Cleans up after testing lookup order filters. * @param cls the closure. */ 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]); } } /** * Runs the tests for lookup order filters. * @param cls the closure. * * @return 0 on success, 1 otherwise. */ 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) { uint64_t order_serial; TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[i], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); order_serial = get_order_serial (&cls->instance, &cls->orders[i]); 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 (order_serial, 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; } /** * Handles all logic for testing lookup order filters. * * @return 0 on success, 1 otherwise. */ static int test_lookup_orders_all_filters (void) { struct TestLookupOrdersAllFilters_Closure test_cls; int test_result; memset (&test_cls, 0, sizeof (test_cls)); pre_test_lookup_orders_all_filters (&test_cls); 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. * * @return 0 on success, 1 otherwise. */ static int run_tests (void) { TEST_RET_ON_FAIL (test_instances ()); TEST_RET_ON_FAIL (test_products ()); TEST_RET_ON_FAIL (test_orders ()); TEST_RET_ON_FAIL (test_deposits ()); TEST_RET_ON_FAIL (test_transfers ()); TEST_RET_ON_FAIL (test_tips ()); TEST_RET_ON_FAIL (test_refunds ()); TEST_RET_ON_FAIL (test_lookup_orders_all_filters ()); 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 (); /* 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; } /** * Entry point for the tests. */ 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 */