From 868a853f0e29c9611542c3e18cb4dfee84779ee3 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Tue, 24 Oct 2017 13:12:11 +0200 Subject: add testcases for merchantdb tipping functions, fix bugs found --- src/backenddb/plugin_merchantdb_postgres.c | 114 ++++++++++---- src/backenddb/test_merchantdb.c | 232 ++++++++++++++++++++++++++++- 2 files changed, 320 insertions(+), 26 deletions(-) diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c index bd764815..92287c4c 100644 --- a/src/backenddb/plugin_merchantdb_postgres.c +++ b/src/backenddb/plugin_merchantdb_postgres.c @@ -188,6 +188,16 @@ postgres_initialize (void *cls) ",balance_curr VARCHAR(" TALER_CURRENCY_LEN_STR ") NOT NULL" ",PRIMARY KEY (reserve_priv)" ");"), + /* table where we remember when tipping reserves where established / enabled */ + GNUNET_PQ_make_execute ("CREATE TABLE IF NOT EXISTS merchant_tip_reserve_credits (" + " reserve_priv BYTEA NOT NULL CHECK (LENGTH(reserve_priv)=32)" + ",credit_uuid BYTEA NOT NULL CHECK (LENGTH(credit_uuid)=64)" + ",timestamp INT8 NOT NULL" + ",amount_val INT8 NOT NULL" + ",amount_frac INT4 NOT NULL" + ",amount_curr VARCHAR(" TALER_CURRENCY_LEN_STR ") NOT NULL" + ",PRIMARY KEY (credit_uuid)" + ");"), /* tips that have been authorized */ GNUNET_PQ_make_execute ("CREATE TABLE IF NOT EXISTS merchant_tips (" " reserve_priv BYTEA NOT NULL CHECK (LENGTH(reserve_priv)=32)" @@ -549,7 +559,8 @@ postgres_initialize (void *cls) 10), GNUNET_PQ_make_prepare ("lookup_reserve_by_tip_id", "SELECT" - " left_val" + " reserve_priv" + ",left_val" ",left_frac" ",left_curr" " FROM merchant_tips" @@ -581,6 +592,17 @@ postgres_initialize (void *cls) ") VALUES " "($1, $2, $3, $4, $5)", 5), + GNUNET_PQ_make_prepare ("insert_tip_credit_uuid", + "INSERT INTO merchant_tip_reserve_credits" + "(reserve_priv" + ",credit_uuid" + ",timestamp" + ",amount_val" + ",amount_frac" + ",amount_curr)" + " VALUES " + "($1, $2, $3, $4, $5, $6)", + 6), GNUNET_PQ_PREPARED_STATEMENT_END }; @@ -2596,19 +2618,8 @@ postgres_enable_tip_reserve (void *cls, struct GNUNET_TIME_Absolute expiration) { struct PostgresClosure *pg = cls; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (reserve_priv), - GNUNET_PQ_query_param_end - }; struct GNUNET_TIME_Absolute old_expiration; struct TALER_Amount old_balance; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_absolute_time ("expiration", - &old_expiration), - TALER_PQ_result_spec_amount ("balance", - &old_balance), - GNUNET_PQ_result_spec_end - }; enum GNUNET_DB_QueryStatus qs; struct GNUNET_TIME_Absolute new_expiration; struct TALER_Amount new_balance; @@ -2620,12 +2631,58 @@ postgres_enable_tip_reserve (void *cls, GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_tip_reserve_balance", - params, - rs); + + /* ensure that credit_uuid is new/unique */ + { + struct GNUNET_TIME_Absolute now; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (reserve_priv), + GNUNET_PQ_query_param_auto_from_type (credit_uuid), + GNUNET_PQ_query_param_absolute_time (&now), + TALER_PQ_query_param_amount (credit), + GNUNET_PQ_query_param_end + }; + + now = GNUNET_TIME_absolute_get (); + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "insert_tip_credit_uuid", + params); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + postgres_rollback (pg); + return qs; + } + /* UUID already exists, we are done! */ + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + postgres_rollback (pg); + return qs; + } + } + + /* Obtain existing reserve balance */ + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (reserve_priv), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_absolute_time ("expiration", + &old_expiration), + TALER_PQ_result_spec_amount ("balance", + &old_balance), + GNUNET_PQ_result_spec_end + }; + + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "lookup_tip_reserve_balance", + params, + rs); + } if (0 > qs) { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); postgres_rollback (pg); return qs; } @@ -2674,11 +2731,16 @@ postgres_enable_tip_reserve (void *cls, params); if (0 > qs) { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); postgres_rollback (pg); return qs; } } - return postgres_commit (pg); + qs = postgres_commit (pg); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; } @@ -2913,16 +2975,18 @@ postgres_pickup_tip (void *cls, ? TALER_EC_TIP_PICKUP_DB_ERROR_HARD : TALER_EC_TIP_PICKUP_DB_ERROR_SOFT; } - if (0 != - TALER_amount_cmp (&existing_amount, - amount)) - { - GNUNET_break_op (0); - postgres_rollback (pg); - return TALER_EC_TIP_PICKUP_AMOUNT_CHANGED; - } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + if (0 != + TALER_amount_cmp (&existing_amount, + amount)) + { + GNUNET_break_op (0); + postgres_rollback (pg); + return TALER_EC_TIP_PICKUP_AMOUNT_CHANGED; + } return TALER_EC_NONE; /* we are done! */ + } } /* Calculate new balance */ diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c index eb4fe14b..05db7c2b 100644 --- a/src/backenddb/test_merchantdb.c +++ b/src/backenddb/test_merchantdb.c @@ -17,6 +17,7 @@ * @file merchant/test_merchantdb_postgres.c * @brief testcase for merchant's postgres db plugin * @author Marcello Stanisci + * @author Christian Grothoff */ #include "platform.h" @@ -509,6 +510,232 @@ test_wire_fee () } +/** + * Test APIs related to tipping. + * + * @return #GNUNET_OK upon success + */ +static int +test_tipping () +{ + struct TALER_ReservePrivateKeyP tip_reserve_priv; + struct TALER_ReservePrivateKeyP pres; + struct GNUNET_HashCode tip_id; + struct GNUNET_HashCode tip_credit_uuid; + struct GNUNET_HashCode pickup_id; + struct GNUNET_TIME_Absolute tip_expiration; + struct GNUNET_TIME_Absolute reserve_expiration; + struct TALER_Amount total; + struct TALER_Amount amount; + struct TALER_Amount inc; + + RND_BLK (&tip_reserve_priv); + if (TALER_EC_TIP_AUTHORIZE_RESERVE_UNKNOWN != + plugin->authorize_tip (plugin->cls, + "testing tips reserve unknown", + &amount, + &tip_reserve_priv, + &tip_expiration, + &tip_id)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + RND_BLK (&tip_credit_uuid); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (CURRENCY ":5", + &total)); + /* Pick short expiration, but long enough to + run 2 DB interactions even on very slow systems. */ + reserve_expiration = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, + 2)); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->enable_tip_reserve (plugin->cls, + &tip_reserve_priv, + &tip_credit_uuid, + &total, + reserve_expiration)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* check idempotency */ + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != + plugin->enable_tip_reserve (plugin->cls, + &tip_reserve_priv, + &tip_credit_uuid, + &total, + reserve_expiration)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* Make sure it has expired, so at this point the value is back at ZERO! */ + sleep (3); + if (TALER_EC_TIP_AUTHORIZE_RESERVE_EXPIRED != + plugin->authorize_tip (plugin->cls, + "testing tips too late", + &amount, + &tip_reserve_priv, + &tip_expiration, + &tip_id)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* Re-add some funds again */ + RND_BLK (&tip_credit_uuid); + reserve_expiration = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, + 2)); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->enable_tip_reserve (plugin->cls, + &tip_reserve_priv, + &tip_credit_uuid, + &total, + reserve_expiration)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* top it up by adding more with a fresh UUID + and even longer expiration time (until end of test) */ + RND_BLK (&tip_credit_uuid); + reserve_expiration = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_DAYS); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->enable_tip_reserve (plugin->cls, + &tip_reserve_priv, + &tip_credit_uuid, + &total, + reserve_expiration)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* Now authorize some tips */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (CURRENCY ":4", + &amount)); + if (TALER_EC_NONE != + plugin->authorize_tip (plugin->cls, + "testing tips", + &amount, + &tip_reserve_priv, + &tip_expiration, + &tip_id)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (tip_expiration.abs_value_us != reserve_expiration.abs_value_us) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (TALER_EC_NONE != + plugin->authorize_tip (plugin->cls, + "testing tips more", + &amount, + &tip_reserve_priv, + &tip_expiration, + &tip_id)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (tip_expiration.abs_value_us != reserve_expiration.abs_value_us) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* Let's try to pick up the authorized tip in 2 increments */ + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (CURRENCY ":2", + &inc)); + RND_BLK (&pickup_id); + if (TALER_EC_NONE != + plugin->pickup_tip (plugin->cls, + &inc, + &tip_id, + &pickup_id, + &pres)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 != memcmp (&pres, + &tip_reserve_priv, + sizeof (pres))) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + RND_BLK (&pickup_id); + if (TALER_EC_NONE != + plugin->pickup_tip (plugin->cls, + &inc, + &tip_id, + &pickup_id, + &pres)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 != memcmp (&pres, + &tip_reserve_priv, + sizeof (pres))) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* Third attempt should fail, as we've picked up 4/4 in amount */ + RND_BLK (&pickup_id); + if (TALER_EC_TIP_PICKUP_NO_FUNDS != + plugin->pickup_tip (plugin->cls, + &inc, + &tip_id, + &pickup_id, + &pres)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* We authorized 8 out of 10, so going for another 4 should fail with insufficient funds */ + if (TALER_EC_TIP_AUTHORIZE_INSUFFICIENT_FUNDS != + plugin->authorize_tip (plugin->cls, + "testing tips insufficient funds", + &amount, + &tip_reserve_priv, + &tip_expiration, + &tip_id)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* Test that picking up with random (unauthorized) tip_id fails as well */ + RND_BLK (&tip_id); + RND_BLK (&pickup_id); + if (TALER_EC_TIP_PICKUP_TIP_ID_UNKNOWN != + plugin->pickup_tip (plugin->cls, + &inc, + &tip_id, + &pickup_id, + &pres)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + /** * Main function that will be run by the scheduler. * @@ -519,6 +746,7 @@ run (void *cls) { struct GNUNET_CONFIGURATION_Handle *cfg = cls; struct GNUNET_TIME_Absolute fake_now; + json_t *out; /* Data for 'store_payment()' */ if (NULL == (plugin = TALER_MERCHANTDB_plugin_load (cfg))) @@ -615,7 +843,6 @@ run (void *cls) &h_contract_terms, &merchant_pub)); - json_t *out; FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->find_contract_terms (plugin->cls, @@ -787,6 +1014,9 @@ run (void *cls) FAILIF (GNUNET_OK != test_wire_fee ()); + FAILIF (GNUNET_OK != + test_tipping ()); + if (-1 == result) result = 0; -- cgit v1.2.3