/* This file is part of TALER (C) 2014-2024 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 test_merchantdb.c * @brief testcase for merchant's postgres db plugin * @author Marcello Stanisci * @author Christian Grothoff * @author Pricilla Huang */ #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; /** * @param test 0 on success, non-zero on failure */ #define TEST_WITH_FAIL_CLAUSE(test, on_fail) \ if ((test)) \ { \ GNUNET_break (0); \ on_fail \ } #define TEST_COND_RET_ON_FAIL(cond, msg) \ if (! (cond)) \ { \ GNUNET_break (0); \ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ msg); \ return 1; \ } /** * @param __test 0 on success, non-zero on failure */ #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) { memset (instance, 0, sizeof (*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"))); instance->instance.use_stefan = true; 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) { memset (account, 0, sizeof (*account)); GNUNET_CRYPTO_hash_create_random (GNUNET_CRYPTO_QUALITY_STRONG, &account->h_wire.hash); GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, &account->salt, sizeof (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)) || (a->use_stefan != b->use_stefan) || (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_memcmp (&a->h_wire, &b->h_wire)) || (0 != GNUNET_memcmp (&a->salt, &b->salt)) || (0 != strcmp (a->payto_uri, b->payto_uri)) || (a->active != b->active)) return 1; return 0; } // FIXME: use this! void lookup_accounts_cb (void *cls, const struct TALER_MERCHANTDB_AccountDetails *account) { const struct TALER_MERCHANTDB_AccountDetails *want = cls; GNUNET_assert (0 == check_accounts_equal (want, account)); } /** * 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 ias instance authentication settings */ 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, const struct TALER_MERCHANTDB_InstanceAuthSettings *ias) { 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) { if (0 != check_instances_equal (cmp->instances_to_cmp[i].instance, is)) continue; 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) { struct TALER_MERCHANTDB_InstanceAuthSettings ias = { 0 }; TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_instance (plugin->cls, &instance->merchant_pub, &instance->merchant_priv, &instance->instance, &ias), "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[GNUNET_NZL (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 InstanceData *instance, const struct TALER_MERCHANTDB_AccountDetails *account, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->inactivate_account (plugin->cls, instance->instance.id, &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")); cls->instances[0].instance.use_stefan = false; 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->instances[0], &cls->accounts[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_inactivate_account (&cls->instances[1], &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; int test_result; pre_test_instances (&test_cls); test_result = run_test_instances (&test_cls); post_test_instances (&test_cls); return test_result; } /* *********** Products ********** */ /** * 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 = GNUNET_strdup (""); GNUNET_assert (NULL != product->product.image); product->product.address = json_array (); GNUNET_assert (NULL != product->product.address); product->product.next_restock = GNUNET_TIME_UNIT_ZERO_TS; } /** * 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); GNUNET_free (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) || (0 != strcmp (a->image, b->image)) || (1 != json_equal (a->address, b->address)) || (GNUNET_TIME_timestamp_cmp (a->next_restock, !=, b->next_restock))) 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"); TALER_MERCHANTDB_product_details_free (&lookup_result); return 1; } const struct TALER_MERCHANTDB_ProductDetails *to_cmp = &product->product; if (0 != check_products_equal (&lookup_result, to_cmp)) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup product failed: incorrect product returned\n"); TALER_MERCHANTDB_product_details_free (&lookup_result); return 1; } TALER_MERCHANTDB_product_details_free (&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_serial DB row ID * @param product_id the identifier of the product found. */ static void lookup_products_cb (void *cls, uint64_t product_serial, const char *product_id) { struct TestLookupProducts_Closure *cmp = cls; GNUNET_assert (product_serial > 0); 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[GNUNET_NZL (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, 0, 20, &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_Timestamp refund_deadline = GNUNET_TIME_relative_to_timestamp (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 = 0; /* will be ignored! */ cls->products[0].product.total_lost = 7; GNUNET_free (cls->products[0].product.image); cls->products[0].product.image = GNUNET_strdup ("image"); 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_timestamp_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 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_Timestamp 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_Timestamp refund_deadline; order->id = order_id; order->contract = json_object (); GNUNET_assert (NULL != order->contract); order->pay_deadline = GNUNET_TIME_relative_to_timestamp ( GNUNET_TIME_UNIT_DAYS); GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, &order->claim_token, sizeof (order->claim_token)); refund_deadline = GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_WEEKS); 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_timestamp (order->pay_deadline)); json_object_set_new (order->contract, "refund_deadline", GNUNET_JSON_from_timestamp (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 TALER_MerchantPostDataHashP 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, NULL, /* session_id */ &h_post, order->pay_deadline, &order->claim_token, order->contract, NULL, 0), "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 TALER_MerchantPostDataHashP oh; struct TALER_MerchantPostDataHashP 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_Timestamp 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[GNUNET_NZL (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_Timestamp 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_TS, .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, false), "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) { uint64_t os; TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_contract_terms (plugin->cls, instance->instance.id, order->id, order->contract, &os), "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, NULL)) { 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 TALER_PrivateContractHashP 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 TALER_PrivateContractHashP h_contract_terms_expected; struct TALER_PrivateContractHashP 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, false, &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 (const char *instance_id, const char *order_id, const char *session_id, bool expected_paid, bool expected_wired) { bool paid; bool wired; bool matches; uint64_t os; TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->lookup_contract_terms3 (plugin->cls, instance_id, order_id, session_id, NULL, &os, &paid, &wired, &matches, NULL), "Lookup payment status failed\n"); if ( (NULL != session_id) && (! matches) ) { paid = false; wired = false; } if (expected_wired != wired) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup payment status failed: wired status is wrong\n"); return 1; } if (expected_paid != paid) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup payment status failed: paid status is wrong\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_TS; GNUNET_assert (0 == json_object_set_new ( cls->orders[2].contract, "pay_deadline", GNUNET_JSON_from_timestamp (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_UNIT_ZERO_TS, .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 TALER_MerchantPostDataHashP 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, NULL)) { 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 TALER_PrivateContractHashP 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 (cls->instance.instance.id, cls->orders[0].id, NULL, false, false)); /* Test lookup order status fails for nonexistent order */ { struct TALER_PrivateContractHashP 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 (cls->instance.instance.id, cls->orders[0].id, NULL, true, false)); TEST_RET_ON_FAIL (test_lookup_payment_status (cls->instance.instance.id, cls->orders[0].id, "test_orders_session", true, false)); TEST_RET_ON_FAIL (test_lookup_payment_status (cls->instance.instance.id, cls->orders[0].id, "bad_session", false, false)); /* 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", false, &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 (cls->instance.instance.id, cls->orders[0].id, 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_Timestamp start_date; /** * When the signing key stops being used. */ struct GNUNET_TIME_Timestamp expire_date; /** * When the signing key becomes invalid for proof. */ struct GNUNET_TIME_Timestamp end_date; }; /** * Creates an exchange signing key. * * @param signkey the signing key data to fill. */ static void make_exchange_signkey (struct ExchangeSignkeyData *signkey) { struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); 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); signkey->start_date = now; signkey->expire_date = now; signkey->end_date = now; TALER_exchange_offline_signkey_validity_sign ( &signkey->exchange_pub, signkey->start_date, signkey->expire_date, signkey->end_date, &signkey->master_priv, &signkey->master_sig); } /** * A container for deposit data. */ struct DepositData { /** * When the deposit was made. */ struct GNUNET_TIME_Timestamp timestamp; /** * Hash of the associated order's contract terms. */ struct TALER_PrivateContractHashP h_contract_terms; /** * Public key of the coin that has been deposited. */ struct TALER_CoinSpendPublicKeyP coin_pub; /** * Signature of the coin that has been deposited. */ struct TALER_CoinSpendSignatureP coin_sig; /** * 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 TALER_MerchantWireHashP 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 GNUNET_TIME_Timestamp now; struct TALER_Amount amount_without_fee; now = GNUNET_TIME_timestamp_get (); deposit->timestamp = now; 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 = "https://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)); GNUNET_assert (0 <= TALER_amount_subtract (&amount_without_fee, &deposit->amount_with_fee, &deposit->deposit_fee)); deposit->h_wire = account->h_wire; GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, &deposit->exchange_sig, sizeof (deposit->exchange_sig)); GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, &deposit->coin_sig, sizeof (deposit->coin_sig)); } /** * 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) { uint64_t row; struct TALER_Amount awf; GNUNET_assert (0 <= TALER_amount_subtract (&awf, &deposit->amount_with_fee, &deposit->deposit_fee)); TEST_COND_RET_ON_FAIL ( expected_result == plugin->insert_deposit_confirmation (plugin->cls, instance->instance.id, deposit->timestamp, &deposit->h_contract_terms, deposit->exchange_url, deposit->timestamp, &awf, &deposit->wire_fee, &deposit->h_wire, &deposit->exchange_sig, &signkey->exchange_pub, &row), "Insert deposit confirmation failed\n"); TEST_COND_RET_ON_FAIL ( expected_result == plugin->insert_deposit (plugin->cls, 0, row, &deposit->coin_pub, &deposit->coin_sig, &deposit->amount_with_fee, &deposit->deposit_fee, &deposit->refund_fee), "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. */ 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) { 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))) { 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 TALER_PrivateContractHashP *h_contract_terms, unsigned int deposits_length, const struct DepositData *deposits) { unsigned int results_matching[GNUNET_NZL (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 TALER_MerchantWireHashP *h_wire, struct GNUNET_TIME_Timestamp deposit_timestamp, struct GNUNET_TIME_Timestamp 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 ((GNUNET_TIME_timestamp_cmp (cmp->deposits_to_cmp[i].timestamp, ==, deposit_timestamp)) && (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 TALER_PrivateContractHashP *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 TALER_MerchantWireHashP *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 TALER_MerchantWireHashP *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 fees charged. */ struct TALER_WireFeeSet fees; /** * Start date of the wire fee. */ struct GNUNET_TIME_Timestamp wire_fee_start; /** * End date of the wire fee. */ struct GNUNET_TIME_Timestamp 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) { 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->fees.wire)); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:0.49", &wire_fee->fees.closing)); wire_fee->wire_fee_start = GNUNET_TIME_timestamp_get (); wire_fee->wire_fee_end = GNUNET_TIME_relative_to_timestamp ( GNUNET_TIME_UNIT_MONTHS); TALER_exchange_offline_wire_fee_sign ( wire_fee->wire_method, wire_fee->wire_fee_start, wire_fee->wire_fee_end, &wire_fee->fees, &signkey->master_priv, &wire_fee->fee_sig); } /** * 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[static deposits_length], struct TransferData *transfer) { struct TALER_TrackTransferDetails *details = NULL; 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; transfer->data.details_length = 0; GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (deposits[0].amount_with_fee.currency, &transfer->data.total_amount)); GNUNET_assert (GNUNET_OK == TALER_amount_set_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_timestamp_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 InstanceData *instance, const struct TransferData *transfer) { struct TALER_Amount total_with_fee; struct TALER_Amount total; struct TALER_Amount fee; struct TALER_Amount exchange_amount; struct GNUNET_TIME_Timestamp time; bool esig; bool verified; if (1 != plugin->lookup_transfer (plugin->cls, instance->instance.id, transfer->exchange_url, &transfer->wtid, &total, &fee, &exchange_amount, &time, &esig, &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)) || (GNUNET_TIME_timestamp_cmp (transfer->data.execution_time, !=, time)) || (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_Timestamp 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)) && (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->fees, 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_WireFeeSet fees; struct GNUNET_TIME_Timestamp start_date; struct GNUNET_TIME_Timestamp 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_timestamp_get (), &fees, &start_date, &end_date, &master_sig)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup wire fee failed\n"); return 1; } if ((0 != TALER_wire_fee_set_cmp (&wire_fee_data->fees, &fees)) || (GNUNET_TIME_timestamp_cmp (wire_fee_data->wire_fee_start, !=, start_date)) || (GNUNET_TIME_timestamp_cmp (wire_fee_data->wire_fee_end, !=, end_date)) || (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_TS * 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_Timestamp 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)) && (GNUNET_TIME_timestamp_cmp ( cmp->transfers_to_cmp[i].data.execution_time, ==, execution_time))) { 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_Timestamp before, struct GNUNET_TIME_Timestamp 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) { GNUNET_array_grow (cls->transfers->data.details, cls->transfers->data.details_length, 0); 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; struct TALER_WireFeeSet fees; /* 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_timestamp_get (), &fees, 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 (cls->instance.instance.id, cls->order.id, NULL, false, true)); TEST_RET_ON_FAIL (test_lookup_transfer (&cls->instance, &cls->transfers[0])); TEST_COND_RET_ON_FAIL ( GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == plugin->set_transfer_status_to_confirmed ( plugin->cls, cls->instance.instance.id, cls->deposit.exchange_url, &cls->transfers[0].wtid, &cls->deposit.amount_with_fee), "Set transfer status to confirmed failed\n"); cls->transfers[0].confirmed = true; TEST_RET_ON_FAIL (test_lookup_transfer (&cls->instance, &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_TS, GNUNET_TIME_UNIT_ZERO_TS, 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; } /** * 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 TALER_PrivateContractHashP *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_Timestamp 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_timestamp_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_Timestamp 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 (((GNUNET_TIME_timestamp_cmp (cmp->refunds_to_cmp[i].timestamp, ==, timestamp)) || ! 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 TALER_PrivateContractHashP *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_Timestamp 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 ((GNUNET_TIME_timestamp_cmp (lookup_cls->refund->timestamp, ==, timestamp)) && (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 TALER_PrivateContractHashP *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) { /* 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)); memset (&cls->refund_proof.exchange_sig, 42, sizeof (cls->refund_proof.exchange_sig)); } /** * 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_new (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_UNIT_ZERO_TS, .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_TS; 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; } static void kyc_status_ok (void *cls, const struct TALER_MerchantWireHashP *h_wire, uint64_t exchange_kyc_serial, const char *payto_uri, const char *exchange_url, struct GNUNET_TIME_Timestamp last_check, bool kyc_ok, enum TALER_AmlDecisionState ades) { bool *fail = cls; if (kyc_ok) *fail = false; } static void kyc_status_fail (void *cls, const struct TALER_MerchantWireHashP *h_wire, uint64_t exchange_kyc_serial, const char *payto_uri, const char *exchange_url, struct GNUNET_TIME_Timestamp last_check, bool kyc_ok, enum TALER_AmlDecisionState ades) { bool *fail = cls; if (! kyc_ok) *fail = false; } /** * Function that tests the KYC table. * * @return 0 on success, 1 otherwise. */ static int test_kyc (void) { struct InstanceData instance; struct TALER_MERCHANTDB_AccountDetails account; bool fail; struct GNUNET_TIME_Timestamp now; make_instance ("test_kyc", &instance); make_account (&account); TEST_RET_ON_FAIL (test_insert_instance (&instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_account (&instance, &account, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); now = GNUNET_TIME_timestamp_get (); TEST_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->account_kyc_set_status (plugin->cls, instance.instance.id, &account.h_wire, "https://exchange.net/", 1LLU, NULL, NULL, now, false, TALER_AML_NORMAL)); TEST_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->account_kyc_set_status (plugin->cls, instance.instance.id, &account.h_wire, "https://exchange2.com/", 1LLU, NULL, NULL, now, false, TALER_AML_NORMAL)); TEST_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->account_kyc_set_status (plugin->cls, instance.instance.id, &account.h_wire, "https://exchange.net/", 1LLU, NULL, NULL, now, true, TALER_AML_NORMAL)); fail = true; TEST_RET_ON_FAIL (1 != plugin->account_kyc_get_status (plugin->cls, instance.instance.id, &account.h_wire, "https://exchange.net/", &kyc_status_ok, &fail)); TEST_RET_ON_FAIL (fail); fail = true; TEST_RET_ON_FAIL (1 != plugin->account_kyc_get_status (plugin->cls, instance.instance.id, NULL, "https://exchange2.com/", &kyc_status_fail, &fail)); TEST_RET_ON_FAIL (fail); fail = true; TEST_RET_ON_FAIL (2 != plugin->account_kyc_get_status (plugin->cls, instance.instance.id, NULL, NULL, &kyc_status_fail, &fail)); TEST_RET_ON_FAIL (fail); fail = true; TEST_RET_ON_FAIL (2 != plugin->account_kyc_get_status (plugin->cls, instance.instance.id, NULL, NULL, &kyc_status_ok, &fail)); TEST_RET_ON_FAIL (fail); json_decref (instance.instance.address); json_decref (instance.instance.jurisdiction); return 0; } /* *********** Templates ********** */ /** * A container for data relevant to a template. */ struct TemplateData { /** * The identifier of the template. */ const char *id; /** * The details of the template. */ struct TALER_MERCHANTDB_TemplateDetails template; }; /** * Creates a template for testing with. * * @param id the id of the template. * @param template the template data to fill. */ static void make_template (const char *id, struct TemplateData *template) { template->id = id; template->template.template_description = "This is a test template"; template->template.otp_id = NULL; template->template.template_contract = json_array (); GNUNET_assert (NULL != template->template.template_contract); } /** * Frees memory associated with @e TemplateData. * * @param template the container to free. */ static void free_template_data (struct TemplateData *template) { GNUNET_free (template->template.otp_id); json_decref (template->template.template_contract); } /** * Compare two templates for equality. * * @param a the first template. * @param b the second template. * @return 0 on equality, 1 otherwise. */ static int check_templates_equal (const struct TALER_MERCHANTDB_TemplateDetails *a, const struct TALER_MERCHANTDB_TemplateDetails *b) { if ((0 != strcmp (a->template_description, b->template_description)) || ( (NULL == a->otp_id) && (NULL != b->otp_id)) || ( (NULL != a->otp_id) && (NULL == b->otp_id)) || ( (NULL != a->otp_id) && (0 != strcmp (a->otp_id, b->otp_id))) || (1 != json_equal (a->template_contract, b->template_contract))) return 1; return 0; } /** * Tests inserting template data into the database. * * @param instance the instance to insert the template for. * @param template the template data to insert. * @param expected_result the result we expect the db to return. * @return 0 when successful, 1 otherwise. */ static int test_insert_template (const struct InstanceData *instance, const struct TemplateData *template, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_template (plugin->cls, instance->instance.id, template->id, 0, &template->template), "Insert template failed\n"); return 0; } /** * Tests updating template data in the database. * * @param instance the instance to update the template for. * @param template the template data to update. * @param expected_result the result we expect the db to return. * @return 0 when successful, 1 otherwise. */ static int test_update_template (const struct InstanceData *instance, const struct TemplateData *template, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->update_template (plugin->cls, instance->instance.id, template->id, &template->template), "Update template failed\n"); return 0; } /** * Tests looking up a template from the db. * * @param instance the instance to query from. * @param template the template to query and compare to. * @return 0 when successful, 1 otherwise. */ static int test_lookup_template (const struct InstanceData *instance, const struct TemplateData *template) { struct TALER_MERCHANTDB_TemplateDetails lookup_result; if (0 > plugin->lookup_template (plugin->cls, instance->instance.id, template->id, &lookup_result)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup template failed\n"); TALER_MERCHANTDB_template_details_free (&lookup_result); return 1; } const struct TALER_MERCHANTDB_TemplateDetails *to_cmp = &template->template; if (0 != check_templates_equal (&lookup_result, to_cmp)) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup template failed: incorrect template returned\n"); TALER_MERCHANTDB_template_details_free (&lookup_result); return 1; } TALER_MERCHANTDB_template_details_free (&lookup_result); return 0; } /** * Closure for testing template lookup */ struct TestLookupTemplates_Closure { /** * Number of template ids to compare to */ unsigned int templates_to_cmp_length; /** * Pointer to array of template ids */ const struct TemplateData *templates_to_cmp; /** * Pointer to array of number of matches for each template */ unsigned int *results_matching; /** * Total number of results returned */ unsigned int results_length; }; /** * Function called after calling @e test_lookup_templates * * @param cls a pointer to the lookup closure. * @param template_id the identifier of the template found. */ static void lookup_templates_cb (void *cls, const char *template_id, const char *template_description) { struct TestLookupTemplates_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; cmp->templates_to_cmp_length > i; ++i) { if ( (0 == strcmp (cmp->templates_to_cmp[i].id, template_id)) && (0 == strcmp (cmp->templates_to_cmp[i].template.template_description, template_description)) ) cmp->results_matching[i] += 1; } } /** * Tests looking up all templates for an instance. * * @param instance the instance to query from. * @param templates_length the number of templates we are expecting. * @param templates the list of templates that we expect to be found. * @return 0 when successful, 1 otherwise. */ static int test_lookup_templates (const struct InstanceData *instance, unsigned int templates_length, const struct TemplateData *templates) { unsigned int results_matching[templates_length]; struct TestLookupTemplates_Closure cls = { .templates_to_cmp_length = templates_length, .templates_to_cmp = templates, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * templates_length); if (0 > plugin->lookup_templates (plugin->cls, instance->instance.id, &lookup_templates_cb, &cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup templates failed\n"); return 1; } if (templates_length != cls.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup templates failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; templates_length > i; ++i) { if (1 != cls.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup templates failed: mismatched data\n"); return 1; } } return 0; } /** * Tests deleting a template. * * @param instance the instance to delete the template from. * @param template the template 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_template (const struct InstanceData *instance, const struct TemplateData *template, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->delete_template (plugin->cls, instance->instance.id, template->id), "Delete template failed\n"); return 0; } /** * Closure for template tests. */ struct TestTemplates_Closure { /** * The instance to use for this test. */ struct InstanceData instance; /** * The array of templates. */ struct TemplateData templates[2]; }; /** * Sets up the data structures used in the template tests. * * @param cls the closure to fill with test data. */ static void pre_test_templates (struct TestTemplates_Closure *cls) { /* Instance */ make_instance ("test_inst_templates", &cls->instance); /* Templates */ make_template ("test_templates_pd_0", &cls->templates[0]); make_template ("test_templates_pd_1", &cls->templates[1]); cls->templates[1].template.template_description = "This is a another test template"; } /** * Handles all teardown after testing. * * @param cls the closure containing memory to be freed. */ static void post_test_templates (struct TestTemplates_Closure *cls) { free_instance_data (&cls->instance); free_template_data (&cls->templates[0]); free_template_data (&cls->templates[1]); } /** * Runs the tests for templates. * * @param cls the container of the test data. * @return 0 on success, 1 otherwise. */ static int run_test_templates (struct TestTemplates_Closure *cls) { /* Test that insert without an instance fails */ TEST_RET_ON_FAIL (test_insert_template (&cls->instance, &cls->templates[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 template */ TEST_RET_ON_FAIL (test_insert_template (&cls->instance, &cls->templates[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test that double insert fails */ TEST_RET_ON_FAIL (test_insert_template (&cls->instance, &cls->templates[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test lookup of individual templates */ TEST_RET_ON_FAIL (test_lookup_template (&cls->instance, &cls->templates[0])); /* Make sure it fails correctly for templates that don't exist */ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->lookup_template (plugin->cls, cls->instance.instance.id, "nonexistent_template", NULL)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup template failed\n"); return 1; } /* Test template update */ cls->templates[0].template.template_description = "This is a test template that has been updated!"; GNUNET_free (cls->templates[0].template.otp_id); cls->templates[0].template.otp_id = GNUNET_strdup ("otp_id"); { /* ensure OTP device exists */ struct TALER_MERCHANTDB_OtpDeviceDetails td = { .otp_description = "my otp", .otp_key = "my key", .otp_algorithm = 1, .otp_ctr = 42 }; GNUNET_assert (0 <= plugin->insert_otp (plugin->cls, cls->instance.instance.id, "otp_id", &td)); } GNUNET_assert (0 == json_array_append_new ( cls->templates[0].template.template_contract, json_string ("This is a test. 3CH."))); TEST_RET_ON_FAIL (test_update_template (&cls->instance, &cls->templates[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_template (&cls->instance, &cls->templates[0])); TEST_RET_ON_FAIL (test_update_template (&cls->instance, &cls->templates[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test collective template lookup */ TEST_RET_ON_FAIL (test_insert_template (&cls->instance, &cls->templates[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_templates (&cls->instance, 2, cls->templates)); /* Test template deletion */ TEST_RET_ON_FAIL (test_delete_template (&cls->instance, &cls->templates[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double deletion fails */ TEST_RET_ON_FAIL (test_delete_template (&cls->instance, &cls->templates[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); TEST_RET_ON_FAIL (test_lookup_templates (&cls->instance, 1, cls->templates)); return 0; } /** * Takes care of template testing. * * @return 0 on success, 1 otherwise. */ static int test_templates (void) { struct TestTemplates_Closure test_cls; int test_result; memset (&test_cls, 0, sizeof (test_cls)); pre_test_templates (&test_cls); test_result = run_test_templates (&test_cls); post_test_templates (&test_cls); return test_result; } /* *********** Webhooks ********** */ /** * A container for data relevant to a webhook. */ struct WebhookData { /** * The identifier of the webhook. */ const char *id; /** * The details of the webhook. */ struct TALER_MERCHANTDB_WebhookDetails webhook; }; /** * Creates a webhook for testing with. * * @param id the id of the webhook. * @param webhook the webhook data to fill. */ static void make_webhook (const char *id, struct WebhookData *webhook) { webhook->id = id; webhook->webhook.event_type = "Paid"; webhook->webhook.url = "https://exampletest.com"; webhook->webhook.http_method = "POST"; webhook->webhook.header_template = "Authorization:XYJAORKJEO"; webhook->webhook.body_template = "$Amount"; } /** * Compare two webhooks for equality. * * @param a the first webhook. * @param b the second webhook. * @return 0 on equality, 1 otherwise. */ static int check_webhooks_equal (const struct TALER_MERCHANTDB_WebhookDetails *a, const struct TALER_MERCHANTDB_WebhookDetails *b) { if ((0 != strcmp (a->event_type, b->event_type)) || (0 != strcmp (a->url, b->url)) || (0 != strcmp (a->http_method, b->http_method)) || (0 != strcmp (a->header_template, b->header_template)) || (0 != strcmp (a->body_template, b->body_template))) return 1; return 0; } /** * Tests inserting webhook data into the database. * * @param instance the instance to insert the webhook for. * @param webhook the webhook data to insert. * @param expected_result the result we expect the db to return. * @return 0 when successful, 1 otherwise. */ static int test_insert_webhook (const struct InstanceData *instance, const struct WebhookData *webhook, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_webhook (plugin->cls, instance->instance.id, webhook->id, &webhook->webhook), "Insert webhook failed\n"); return 0; } /** * Tests updating webhook data in the database. * * @param instance the instance to update the webhook for. * @param webhook the webhook data to update. * @param expected_result the result we expect the db to return. * @return 0 when successful, 1 otherwise. */ static int test_update_webhook (const struct InstanceData *instance, const struct WebhookData *webhook, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->update_webhook (plugin->cls, instance->instance.id, webhook->id, &webhook->webhook), "Update webhook failed\n"); return 0; } /** * Tests looking up a webhook from the db. * * @param instance the instance to query from. * @param webhook the webhook to query and compare to. * @return 0 when successful, 1 otherwise. */ static int test_lookup_webhook (const struct InstanceData *instance, const struct WebhookData *webhook) { struct TALER_MERCHANTDB_WebhookDetails lookup_result; if (0 > plugin->lookup_webhook (plugin->cls, instance->instance.id, webhook->id, &lookup_result)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup webhook failed\n"); TALER_MERCHANTDB_webhook_details_free (&lookup_result); return 1; } const struct TALER_MERCHANTDB_WebhookDetails *to_cmp = &webhook->webhook; if (0 != check_webhooks_equal (&lookup_result, to_cmp)) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup webhook failed: incorrect webhook returned\n"); TALER_MERCHANTDB_webhook_details_free (&lookup_result); return 1; } TALER_MERCHANTDB_webhook_details_free (&lookup_result); return 0; } /** * Closure for testing webhook lookup */ struct TestLookupWebhooks_Closure { /** * Number of webhook ids to compare to */ unsigned int webhooks_to_cmp_length; /** * Pointer to array of webhook ids */ const struct WebhookData *webhooks_to_cmp; /** * Pointer to array of number of matches for each webhook */ unsigned int *results_matching; /** * Total number of results returned */ unsigned int results_length; }; /** * Function called after calling @e test_lookup_webhooks * * @param cls a pointer to the lookup closure. * @param webhook_id the identifier of the webhook found. */ static void lookup_webhooks_cb (void *cls, const char *webhook_id, const char *event_type) { struct TestLookupWebhooks_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; cmp->webhooks_to_cmp_length > i; ++i) { if ((0 == strcmp (cmp->webhooks_to_cmp[i].id, webhook_id)) && (0 == strcmp (cmp->webhooks_to_cmp[i].webhook.event_type, event_type)) ) cmp->results_matching[i] += 1; } } /** * Tests looking up all webhooks for an instance. * * @param instance the instance to query from. * @param webhooks_length the number of webhooks we are expecting. * @param webhooks the list of webhooks that we expect to be found. * @return 0 when successful, 1 otherwise. */ static int test_lookup_webhooks (const struct InstanceData *instance, unsigned int webhooks_length, const struct WebhookData *webhooks) { unsigned int results_matching[webhooks_length]; struct TestLookupWebhooks_Closure cls = { .webhooks_to_cmp_length = webhooks_length, .webhooks_to_cmp = webhooks, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * webhooks_length); if (0 > plugin->lookup_webhooks (plugin->cls, instance->instance.id, &lookup_webhooks_cb, &cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup webhooks failed\n"); return 1; } if (webhooks_length != cls.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup webhooks failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; webhooks_length > i; ++i) { if (1 != cls.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup webhooks failed: mismatched data\n"); return 1; } } return 0; } /** * Function called after calling @e test_lookup_webhooks * * @param cls a pointer to the lookup closure. * @param webhook_id the identifier of the webhook found. */ static void lookup_webhook_by_event_cb (void *cls, uint64_t webhook_serial, const char *event_type, const char *url, const char *http_method, const char *header_template, const char *body_template) { struct TestLookupWebhooks_Closure *cmp = cls; if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; cmp->webhooks_to_cmp_length > i; ++i) { if ((0 == strcmp (cmp->webhooks_to_cmp[i].webhook.event_type, event_type)) && (0 == strcmp (cmp->webhooks_to_cmp[i].webhook.url, url)) && (0 == strcmp (cmp->webhooks_to_cmp[i].webhook.http_method, http_method)) && (0 == strcmp (cmp->webhooks_to_cmp[i].webhook.header_template, header_template)) && (0 == strcmp (cmp->webhooks_to_cmp[i].webhook.body_template, body_template)) ) cmp->results_matching[i] += 1; } } /** * Tests looking up webhooks by event for an instance. * * @param instance the instance to query from. * @param webhooks_length the number of webhooks we are expecting. * @param webhooks the list of webhooks that we expect to be found. * @return 0 when successful, 1 otherwise. */ static int test_lookup_webhook_by_event (const struct InstanceData *instance, unsigned int webhooks_length, const struct WebhookData *webhooks) { unsigned int results_matching[webhooks_length]; struct TestLookupWebhooks_Closure cls = { .webhooks_to_cmp_length = webhooks_length, .webhooks_to_cmp = webhooks, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * webhooks_length); if (0 > plugin->lookup_webhook_by_event (plugin->cls, instance->instance.id, webhooks->webhook.event_type, &lookup_webhook_by_event_cb, &cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup webhooks by event failed\n"); return 1; } if (webhooks_length != cls.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup webhooks by event failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; webhooks_length > i; ++i) { if (1 != cls.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup webhooks by event failed: mismatched data\n"); return 1; } } return 0; } /** * Tests deleting a webhook. * * @param instance the instance to delete the webhook from. * @param webhook the webhook 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_webhook (const struct InstanceData *instance, const struct WebhookData *webhook, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->delete_webhook (plugin->cls, instance->instance.id, webhook->id), "Delete webhook failed\n"); return 0; } /** * Closure for webhook tests. */ struct TestWebhooks_Closure { /** * The instance to use for this test. */ struct InstanceData instance; /** * The array of webhooks. */ struct WebhookData webhooks[3]; }; /** * Sets up the data structures used in the webhook tests. * * @param cls the closure to fill with test data. */ static void pre_test_webhooks (struct TestWebhooks_Closure *cls) { /* Instance */ make_instance ("test_inst_webhooks", &cls->instance); /* Webhooks */ make_webhook ("test_webhooks_wb_0", &cls->webhooks[0]); make_webhook ("test_webhooks_wb_1", &cls->webhooks[1]); cls->webhooks[1].webhook.event_type = "Test paid"; cls->webhooks[1].webhook.url = "https://example.com"; cls->webhooks[1].webhook.http_method = "POST"; cls->webhooks[1].webhook.header_template = "Authorization:1XYJAOR493O"; cls->webhooks[1].webhook.body_template = "$Amount"; make_webhook ("test_webhooks_wb_2", &cls->webhooks[2]); cls->webhooks[2].webhook.event_type = "Test paid"; cls->webhooks[2].webhook.url = "https://examplerefund.com"; cls->webhooks[2].webhook.http_method = "POST"; cls->webhooks[2].webhook.header_template = "Authorization:XY6ORK52JEO"; cls->webhooks[2].webhook.body_template = "$Amount"; } /** * Handles all teardown after testing. * * @param cls the closure containing memory to be freed. */ static void post_test_webhooks (struct TestWebhooks_Closure *cls) { free_instance_data (&cls->instance); } /** * Runs the tests for webhooks. * * @param cls the container of the test data. * @return 0 on success, 1 otherwise. */ static int run_test_webhooks (struct TestWebhooks_Closure *cls) { /* Test that insert without an instance fails */ TEST_RET_ON_FAIL (test_insert_webhook (&cls->instance, &cls->webhooks[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 webhook */ TEST_RET_ON_FAIL (test_insert_webhook (&cls->instance, &cls->webhooks[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test that double insert fails */ TEST_RET_ON_FAIL (test_insert_webhook (&cls->instance, &cls->webhooks[0], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test lookup of individual webhooks */ TEST_RET_ON_FAIL (test_lookup_webhook (&cls->instance, &cls->webhooks[0])); /* Make sure it fails correctly for webhooks that don't exist */ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->lookup_webhook (plugin->cls, cls->instance.instance.id, "nonexistent_webhook", NULL)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup webhook failed\n"); return 1; } /* Test webhook update */ cls->webhooks[0].webhook.event_type = "Test paid"; cls->webhooks[0].webhook.url = "example.com"; cls->webhooks[0].webhook.http_method = "POST"; cls->webhooks[0].webhook.header_template = "Authorization:WEKFOEKEXZ"; cls->webhooks[0].webhook.body_template = "$Amount"; TEST_RET_ON_FAIL (test_update_webhook (&cls->instance, &cls->webhooks[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_webhook (&cls->instance, &cls->webhooks[0])); TEST_RET_ON_FAIL (test_update_webhook (&cls->instance, &cls->webhooks[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); /* Test collective webhook lookup */ TEST_RET_ON_FAIL (test_insert_webhook (&cls->instance, &cls->webhooks[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_webhooks (&cls->instance, 2, cls->webhooks)); TEST_RET_ON_FAIL (test_lookup_webhook_by_event (&cls->instance, 2, cls->webhooks)); TEST_RET_ON_FAIL (test_insert_webhook (&cls->instance, &cls->webhooks[2], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test webhook deletion */ TEST_RET_ON_FAIL (test_delete_webhook (&cls->instance, &cls->webhooks[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double deletion fails */ TEST_RET_ON_FAIL (test_delete_webhook (&cls->instance, &cls->webhooks[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); cls->webhooks[1] = cls->webhooks[2]; TEST_RET_ON_FAIL (test_lookup_webhooks (&cls->instance, 2, cls->webhooks)); TEST_RET_ON_FAIL (test_lookup_webhook_by_event (&cls->instance, 2, cls->webhooks)); return 0; } /** * Takes care of webhook testing. * * @return 0 on success, 1 otherwise. */ static int test_webhooks (void) { struct TestWebhooks_Closure test_cls; pre_test_webhooks (&test_cls); int test_result = run_test_webhooks (&test_cls); post_test_webhooks (&test_cls); return test_result; } /* *********** Pending Webhooks ********** */ /** * A container for data relevant to a pending webhook. */ struct PendingWebhookData { /** * Reference to the configured webhook template. */ uint64_t webhook_serial; /** * The details of the pending webhook. */ struct TALER_MERCHANTDB_PendingWebhookDetails pwebhook; }; /** * Creates a pending webhook for testing with. * * @param serial reference to the configured webhook template. * @param pwebhook the pending webhook data to fill. */ static void make_pending_webhook (uint64_t webhook_serial, struct PendingWebhookData *pwebhook) { pwebhook->webhook_serial = webhook_serial; pwebhook->pwebhook.next_attempt = GNUNET_TIME_UNIT_ZERO_ABS; pwebhook->pwebhook.retries = 0; pwebhook->pwebhook.url = "https://exampletest.com"; pwebhook->pwebhook.http_method = "POST"; pwebhook->pwebhook.header = "Authorization:XYJAORKJEO"; pwebhook->pwebhook.body = "$Amount"; } /** * Tests inserting pending webhook data into the database. * * @param instance the instance to insert the pending webhook for. * @param pending webhook the pending webhook data to insert. * @param expected_result the result we expect the db to return. * @return 0 when successful, 1 otherwise. */ static int test_insert_pending_webhook (const struct InstanceData *instance, struct PendingWebhookData *pwebhook, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_pending_webhook (plugin->cls, instance->instance.id, pwebhook-> webhook_serial, pwebhook->pwebhook.url, pwebhook->pwebhook. http_method, pwebhook->pwebhook. header, pwebhook->pwebhook.body), "Insert pending webhook failed\n"); return 0; } /** * Tests updating pending webhook data in the database. * * @param instance the instance to update the pending webhook for. * @param pending webhook the pending webhook data to update. * @param expected_result the result we expect the db to return. * @return 0 when successful, 1 otherwise. */ static int test_update_pending_webhook (const struct InstanceData *instance, struct PendingWebhookData *pwebhook, enum GNUNET_DB_QueryStatus expected_result) { pwebhook->pwebhook.next_attempt = GNUNET_TIME_relative_to_absolute ( GNUNET_TIME_UNIT_HOURS); pwebhook->pwebhook.retries++; TEST_COND_RET_ON_FAIL (expected_result == plugin->update_pending_webhook (plugin->cls, pwebhook-> webhook_serial, pwebhook->pwebhook. next_attempt), "Update pending webhook failed\n"); return 0; } /** * Container for information for looking up the row number of a deposit. */ struct LookupPendingWebhookSerial_Closure { /** * The pending webhook we're looking for. */ const struct PendingWebhookData *pwebhook; /** * The serial found. */ uint64_t webhook_pending_serial; }; /** * Function called after calling @e test_lookup_all_webhook, * test_lookup_future_webhook and test_lookup_pending_webhook * * @param cls a pointer to the lookup closure. * @param webhook_serial reference to the configured webhook template. */ static void get_pending_serial_cb (void *cls, uint64_t webhook_pending_serial, struct GNUNET_TIME_Absolute next_attempt, uint32_t retries, const char *url, const char *http_method, const char *header, const char *body) { struct LookupPendingWebhookSerial_Closure *lpw = cls; if ((0 == strcmp (lpw->pwebhook->pwebhook.url, url)) && (0 == strcmp (lpw->pwebhook->pwebhook.http_method, http_method)) && (0 == strcmp (lpw->pwebhook->pwebhook.header, header)) && (0 == strcmp (lpw->pwebhook->pwebhook.body, body)) ) { lpw->webhook_pending_serial = webhook_pending_serial; } /* else { fprintf(stdout, "next_attempt: %lu vs %lu\n", lpw->pwebhook->pwebhook.next_attempt.abs_value_us, next_attempt.abs_value_us); fprintf(stdout, "retries: %d vs %d\n", lpw->pwebhook->pwebhook.retries, retries); fprintf(stdout, "url: %s vs %s\n", lpw->pwebhook->pwebhook.url, url); fprintf(stdout, "http_method: %s vs %s\n", lpw->pwebhook->pwebhook.http_method, http_method); fprintf(stdout, "header: %s vs %s\n", lpw->pwebhook->pwebhook.header, header); fprintf(stdout, "body: %s vs %s\n", lpw->pwebhook->pwebhook.body, body); }*/ } /** * Convenience function to retrieve the row number of a webhook pending in the database. * * @param instance the instance to get webhook pending(wp) from. * @param webhook pending the wp to lookup the serial for. * @return the row number of the deposit. */ static uint64_t get_pending_serial (const struct InstanceData *instance, const struct PendingWebhookData *pwebhook) { struct LookupPendingWebhookSerial_Closure lpw = { .pwebhook = pwebhook, .webhook_pending_serial = 0 }; GNUNET_assert (0 < plugin->lookup_all_webhooks (plugin->cls, instance->instance.id, 0, INT_MAX, &get_pending_serial_cb, &lpw)); GNUNET_assert (0 != lpw.webhook_pending_serial); return lpw.webhook_pending_serial; } /** * Closure for testing pending webhook lookup */ struct TestLookupPendingWebhooks_Closure { /** * Number of webhook serial to compare to */ unsigned int webhooks_to_cmp_length; /** * Pointer to array of webhook serials */ const struct PendingWebhookData *webhooks_to_cmp; /** * Pointer to array of number of matches for each pending webhook */ unsigned int *results_matching; /** * Total number of results returned */ unsigned int results_length; }; /** * Function called after calling @e test_lookup_all_webhook, * test_lookup_future_webhook and test_lookup_pending_webhook * * @param cls a pointer to the lookup closure. * @param webhook_serial reference to the configured webhook template. */ static void lookup_pending_webhooks_cb (void *cls, uint64_t webhook_pending_serial, struct GNUNET_TIME_Absolute next_attempt, uint32_t retries, const char *url, const char *http_method, const char *header, const char *body) { struct TestLookupPendingWebhooks_Closure *cmp = cls; cmp->results_length++; for (unsigned int i = 0; cmp->webhooks_to_cmp_length > i; ++i) { if ((0 == strcmp (cmp->webhooks_to_cmp[i].pwebhook.url, url)) && (0 == strcmp (cmp->webhooks_to_cmp[i].pwebhook.http_method, http_method)) && (0 == strcmp (cmp->webhooks_to_cmp[i].pwebhook.header, header)) && (0 == strcmp (cmp->webhooks_to_cmp[i].pwebhook.body, body)) ) { cmp->results_matching[i]++; } } } /** * Tests looking up the pending webhook for an instance. * * @param instance the instance to query from. * @param pwebhooks_length the number of pending webhook we are expecting. * @param pwebhooks the list of pending webhooks that we expect to be found. * @return 0 when successful, 1 otherwise. */ static int test_lookup_pending_webhooks (const struct InstanceData *instance, unsigned int pwebhooks_length, const struct PendingWebhookData *pwebhooks) { unsigned int results_matching[pwebhooks_length]; struct TestLookupPendingWebhooks_Closure cls = { .webhooks_to_cmp_length = pwebhooks_length, .webhooks_to_cmp = pwebhooks, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (results_matching)); if (0 > plugin->lookup_pending_webhooks (plugin->cls, &lookup_pending_webhooks_cb, &cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pending webhook failed\n"); return 1; } if (pwebhooks_length != cls.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pending webhook failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; i < pwebhooks_length; i++) { if (1 != cls.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup pending webhook failed: mismatched data\n"); return 1; } } return 0; } /** * Tests looking up the future webhook to send for an instance. * * @param instance the instance to query from. * @param pwebhooks_length the number of pending webhook we are expecting. * @param pwebhooks the list of pending webhooks that we expect to be found. * @return 0 when successful, 1 otherwise. */ static int test_lookup_future_webhook (const struct InstanceData *instance, unsigned int pwebhooks_length, const struct PendingWebhookData *pwebhooks) { unsigned int results_matching[pwebhooks_length]; struct TestLookupPendingWebhooks_Closure cls = { .webhooks_to_cmp_length = pwebhooks_length, .webhooks_to_cmp = pwebhooks, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (results_matching)); if (0 > plugin->lookup_future_webhook (plugin->cls, &lookup_pending_webhooks_cb, &cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup future webhook failed\n"); return 1; } if (pwebhooks_length != cls.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup future webhook failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; pwebhooks_length > i; ++i) { if (1 != cls.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup future webhook failed: mismatched data\n"); return 1; } } return 0; } /** * Tests looking up all the pending webhook for an instance. * * @param instance the instance to query from. * @param pwebhooks_length the number of pending webhook we are expecting. * @param pwebhooks the list of pending webhooks that we expect to be found. * @return 0 when successful, 1 otherwise. */ static int test_lookup_all_webhooks (const struct InstanceData *instance, unsigned int pwebhooks_length, const struct PendingWebhookData *pwebhooks) { uint64_t max_results = 2; uint64_t min_row = 0; unsigned int results_matching[GNUNET_NZL (pwebhooks_length)]; struct TestLookupPendingWebhooks_Closure cls = { .webhooks_to_cmp_length = pwebhooks_length, .webhooks_to_cmp = pwebhooks, .results_matching = results_matching, .results_length = 0 }; memset (results_matching, 0, sizeof (results_matching)); if (0 > plugin->lookup_all_webhooks (plugin->cls, instance->instance.id, min_row, max_results, &lookup_pending_webhooks_cb, &cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup all webhooks failed\n"); return 1; } if (pwebhooks_length != cls.results_length) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup all webhooks failed: incorrect number of results\n"); return 1; } for (unsigned int i = 0; pwebhooks_length > i; ++i) { if (1 != cls.results_matching[i]) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Lookup all webhooks failed: mismatched data\n"); return 1; } } return 0; } /** * Tests deleting a pending webhook. * * @param instance the instance to delete the pending webhook from. * @param pwebhook the pending webhook 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_pending_webhook (uint64_t webhooks_pending_serial, enum GNUNET_DB_QueryStatus expected_result) { TEST_COND_RET_ON_FAIL (expected_result == plugin->delete_pending_webhook (plugin->cls, webhooks_pending_serial), "Delete webhook failed\n"); return 0; } /** * Closure for pending webhook tests. */ struct TestPendingWebhooks_Closure { /** * The instance to use for this test. */ struct InstanceData instance; /** * The array of pending webhooks. */ struct PendingWebhookData pwebhooks[2]; }; /** * Sets up the data structures used in the pending webhook tests. * * @param cls the closure to fill with test data. */ static void pre_test_pending_webhooks (struct TestPendingWebhooks_Closure *cls) { /* Instance */ make_instance ("test_inst_pending_webhooks", &cls->instance); /* Webhooks */ make_pending_webhook (1, &cls->pwebhooks[0]); make_pending_webhook (4, &cls->pwebhooks[1]); cls->pwebhooks[1].pwebhook.url = "https://test.com"; cls->pwebhooks[1].pwebhook.http_method = "POST"; cls->pwebhooks[1].pwebhook.header = "Authorization:XYJAO5R06EO"; cls->pwebhooks[1].pwebhook.body = "$Amount"; } /** * Handles all teardown after testing. * * @param cls the closure containing memory to be freed. */ static void post_test_pending_webhooks (struct TestPendingWebhooks_Closure *cls) { free_instance_data (&cls->instance); } /** * Runs the tests for pending webhooks. * * @param cls the container of the test data. * @return 0 on success, 1 otherwise. */ static int run_test_pending_webhooks (struct TestPendingWebhooks_Closure *cls) { /* Test that insert without an instance fails */ TEST_RET_ON_FAIL (test_insert_pending_webhook (&cls->instance, &cls->pwebhooks[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 pending webhook */ TEST_RET_ON_FAIL (test_insert_pending_webhook (&cls->instance, &cls->pwebhooks[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_insert_pending_webhook (&cls->instance, &cls->pwebhooks[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test collective pending webhook lookup */ TEST_RET_ON_FAIL (test_lookup_pending_webhooks (&cls->instance, 2, cls->pwebhooks)); /* Test pending webhook update */ TEST_RET_ON_FAIL (test_update_pending_webhook (&cls->instance, &cls->pwebhooks[0], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_future_webhook (&cls->instance, 1, &cls->pwebhooks[1])); TEST_RET_ON_FAIL (test_update_pending_webhook (&cls->instance, &cls->pwebhooks[1], GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); // ??? TEST_RET_ON_FAIL (test_lookup_all_webhooks (&cls->instance, 2, cls->pwebhooks)); uint64_t webhook_pending_serial0 = get_pending_serial (&cls->instance, &cls->pwebhooks[0]); uint64_t webhook_pending_serial1 = get_pending_serial (&cls->instance, &cls->pwebhooks[1]); /* Test webhook deletion */ TEST_RET_ON_FAIL (test_delete_pending_webhook (webhook_pending_serial1, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test double deletion fails */ TEST_RET_ON_FAIL (test_delete_pending_webhook (webhook_pending_serial1, GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); TEST_RET_ON_FAIL (test_delete_pending_webhook (webhook_pending_serial0, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); TEST_RET_ON_FAIL (test_lookup_all_webhooks (&cls->instance, 0, NULL)); return 0; } /** * Takes care of pending webhook testing. * * @return 0 on success, 1 otherwise. */ static int test_pending_webhooks (void) { struct TestPendingWebhooks_Closure test_cls; pre_test_pending_webhooks (&test_cls); int test_result = run_test_pending_webhooks (&test_cls); post_test_pending_webhooks (&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_refunds ()); TEST_RET_ON_FAIL (test_lookup_orders_all_filters ()); TEST_RET_ON_FAIL (test_kyc ()); TEST_RET_ON_FAIL (test_templates ()); TEST_RET_ON_FAIL (test_webhooks ()); TEST_RET_ON_FAIL (test_pending_webhooks ()); 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; } (void) plugin->drop_tables (plugin->cls); if (GNUNET_OK != plugin->create_tables (plugin->cls)) { result = 77; return; } if (GNUNET_OK != plugin->connect (plugin->cls)) { result = 17; return; } /* Run the preflight */ plugin->preflight (plugin->cls); result = run_tests (); if (0 == result) /** result = run_test_templates (); if (0 == result)*/ { /* Test dropping tables */ if (GNUNET_OK != plugin->drop_tables (plugin->cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Dropping tables failed\n"); result = 77; return; } } 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); fprintf (stdout, "Using %s\n", config_filename); 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 */