anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

test_redux_codec.c (10730B)


      1 /*
      2   This file is part of Anastasis
      3   Copyright (C) 2022 Anastasis SARL
      4 
      5   Anastasis is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13   You should have received a copy of the GNU General Public License along with
     14   Anastasis; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file reducer/test_redux_codec.c
     18  * @brief tests for the typed reducer state codec
     19  * @author Christian Grothoff
     20  *
     21  * Checks three properties of ANASTASIS_REDUX_state_parse_() and
     22  * ANASTASIS_REDUX_state_serialize_():
     23  *
     24  * - every state the CLI test suite feeds the reducer parses;
     25  * - the codec is idempotent (serialize . parse . serialize == serialize .
     26  *   parse), so no field is lost or invented on the way through;
     27  * - unknown fields are rejected rather than silently dropped.
     28  *
     29  * The codec is also exercised end-to-end against live states by the `cli`
     30  * suite, which drives the real reducer; this test is the fast, offline part
     31  * that needs neither a database nor a provider.
     32  */
     33 #include "platform.h"
     34 #include "anastasis_redux.h"
     35 #include "anastasis_api_redux.h"
     36 #include "anastasis_api_redux_state.h"
     37 #include <taler/taler_json_lib.h>
     38 
     39 
     40 /**
     41  * Return value of the test, 0 on success.
     42  */
     43 static int global_ret;
     44 
     45 
     46 /**
     47  * Check that @a json parses and that the codec is idempotent on it.
     48  *
     49  * @param name name of the input, for error messages
     50  * @param json state to check
     51  */
     52 static void
     53 check_idempotent (const char *name,
     54                   const json_t *json)
     55 {
     56   struct ANASTASIS_ReduxState *rs;
     57   enum TALER_ErrorCode ec;
     58   const char *detail;
     59   json_t *once;
     60   json_t *twice;
     61 
     62   rs = ANASTASIS_REDUX_state_parse_ (json,
     63                                      &ec,
     64                                      &detail);
     65   if (NULL == rs)
     66   {
     67     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     68                 "%s: failed to parse at `%s'\n",
     69                 name,
     70                 detail);
     71     global_ret = 1;
     72     return;
     73   }
     74   once = ANASTASIS_REDUX_state_serialize_ (rs);
     75   GNUNET_assert (NULL != once);
     76   ANASTASIS_REDUX_state_free_ (rs);
     77 
     78   rs = ANASTASIS_REDUX_state_parse_ (once,
     79                                      &ec,
     80                                      &detail);
     81   if (NULL == rs)
     82   {
     83     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     84                 "%s: own output failed to re-parse at `%s'\n",
     85                 name,
     86                 detail);
     87     json_decref (once);
     88     global_ret = 1;
     89     return;
     90   }
     91   twice = ANASTASIS_REDUX_state_serialize_ (rs);
     92   GNUNET_assert (NULL != twice);
     93   ANASTASIS_REDUX_state_free_ (rs);
     94 
     95   if (1 != json_equal (once,
     96                        twice))
     97   {
     98     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     99                 "%s: codec is not idempotent\n",
    100                 name);
    101     json_dumpf (once,
    102                 stderr,
    103                 JSON_INDENT (2) | JSON_SORT_KEYS);
    104     json_dumpf (twice,
    105                 stderr,
    106                 JSON_INDENT (2) | JSON_SORT_KEYS);
    107     global_ret = 1;
    108   }
    109   json_decref (once);
    110   json_decref (twice);
    111 }
    112 
    113 
    114 /**
    115  * Check that @a json is rejected by the parser.
    116  *
    117  * @param name name of the input, for error messages
    118  * @param json state that must not parse
    119  */
    120 static void
    121 check_rejected (const char *name,
    122                 const json_t *json)
    123 {
    124   struct ANASTASIS_ReduxState *rs;
    125   enum TALER_ErrorCode ec;
    126   const char *detail;
    127 
    128   rs = ANASTASIS_REDUX_state_parse_ (json,
    129                                      &ec,
    130                                      &detail);
    131   if (NULL != rs)
    132   {
    133     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    134                 "%s: expected rejection, but the state parsed\n",
    135                 name);
    136     ANASTASIS_REDUX_state_free_ (rs);
    137     global_ret = 1;
    138     return;
    139   }
    140   if (TALER_EC_ANASTASIS_REDUCER_STATE_INVALID != ec)
    141   {
    142     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    143                 "%s: rejected with unexpected error code %d\n",
    144                 name,
    145                 (int) ec);
    146     global_ret = 1;
    147   }
    148 }
    149 
    150 
    151 /**
    152  * Run the codec over every state fixture the CLI tests use.
    153  *
    154  * @param srcdir directory holding the `resources/` fixtures
    155  */
    156 static void
    157 test_fixtures (const char *srcdir)
    158 {
    159   static const char *const files[] = {
    160     "00-backup.json",
    161     "01-backup.json",
    162     "02-backup.json",
    163     "03-backup.json",
    164     "04-backup.json",
    165     "05-backup.json",
    166     "06-backup.json",
    167     "00-recovery.json",
    168     "01-recovery.json",
    169     "02-recovery.json",
    170     NULL
    171   };
    172 
    173   for (unsigned int i = 0; NULL != files[i]; i++)
    174   {
    175     json_error_t err;
    176     json_t *json;
    177     char *fn;
    178 
    179     GNUNET_asprintf (&fn,
    180                      "%s/resources/%s",
    181                      srcdir,
    182                      files[i]);
    183     json = json_load_file (fn,
    184                            JSON_REJECT_DUPLICATES,
    185                            &err);
    186     if (NULL == json)
    187     {
    188       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    189                   "Failed to load `%s': %s\n",
    190                   fn,
    191                   err.text);
    192       GNUNET_free (fn);
    193       global_ret = 1;
    194       continue;
    195     }
    196     check_idempotent (files[i],
    197                       json);
    198     json_decref (json);
    199     GNUNET_free (fn);
    200   }
    201 }
    202 
    203 
    204 /**
    205  * Check that every challenge feedback variant survives the codec.
    206  * These are the states with the most structure (a tagged union), and
    207  * several of them only occur against a live provider, so they are
    208  * covered here rather than by the CLI tests.
    209  */
    210 static void
    211 test_challenge_feedback (void)
    212 {
    213   static const char *const variants[] = {
    214     "{\"state\":\"code-in-file\",\"filename\":\"/tmp/tan\","
    215     "\"display_hint\":\"look here\"}",
    216     "{\"state\":\"send-to-address\",\"address_hint\":\"a@b.c\","
    217     "\"display_hint\":\"sent\"}",
    218     "{\"state\":\"send-to-address\",\"display_hint\":\"already sent\"}",
    219     "{\"state\":\"taler-payment\",\"taler_pay_uri\":\"taler://pay/x\","
    220     "\"provider\":\"http://localhost:8086/\",\"display_hint\":\"pay\","
    221     "\"payment_secret\":\"3F1QF6ZY2H0V4CQNVQ8YV2P8V0Y0EWWG14JBQZ3M4P2C1Y8XM53G\"}",
    222     "{\"state\":\"server-failure\",\"http_status\":500,\"error_code\":1}",
    223     "{\"state\":\"truth-unknown\",\"http_status\":404,\"error_code\":8108}",
    224     "{\"state\":\"iban-instructions\",\"target_iban\":\"DE12\","
    225     "\"target_business_name\":\"Bank\",\"wire_transfer_subject\":\"subj\","
    226     "\"challenge_amount\":\"TESTKUDOS:1\",\"display_hint\":\"wire it\"}",
    227     "{\"state\":\"solved\"}",
    228     "{\"state\":\"incorrect-answer\",\"error_code\":8110}",
    229     "{\"state\":\"rate-limit-exceeded\",\"error_code\":8111,"
    230     "\"request_limit\":3,\"request_frequency\":{\"d_us\":1000000},"
    231     "\"display_hint\":\"slow down\"}",
    232     NULL
    233   };
    234   static const char *const uuid
    235     = "3AHRZ6DMV29GQY5CKT18FPX4BJS07ENW3AHRZ6DMV29GQY5CKT18";
    236 
    237   for (unsigned int i = 0; NULL != variants[i]; i++)
    238   {
    239     json_error_t err;
    240     json_t *fb;
    241     json_t *state;
    242 
    243     fb = json_loads (variants[i],
    244                      JSON_REJECT_DUPLICATES,
    245                      &err);
    246     GNUNET_assert (NULL != fb);
    247     state = GNUNET_JSON_PACK (
    248       GNUNET_JSON_pack_string ("reducer_type",
    249                                "recovery"),
    250       GNUNET_JSON_pack_string ("recovery_state",
    251                                "CHALLENGE_SOLVING"));
    252     {
    253       json_t *cf = json_object ();
    254 
    255       GNUNET_assert (NULL != cf);
    256       GNUNET_assert (0 ==
    257                      json_object_set_new (cf,
    258                                           uuid,
    259                                           fb));
    260       GNUNET_assert (0 ==
    261                      json_object_set_new (state,
    262                                           "challenge_feedback",
    263                                           cf));
    264     }
    265     check_idempotent (variants[i],
    266                       state);
    267     json_decref (state);
    268   }
    269 }
    270 
    271 
    272 /**
    273  * Check that fields the reducer does not know about are rejected
    274  * instead of being silently carried along or dropped.
    275  */
    276 static void
    277 test_strictness (void)
    278 {
    279   static const char *const bad[] = {
    280     /* unknown field at the top level (this is the shape the TypeScript
    281        reducer produces; see dold.md) */
    282     "{\"reducer_type\":\"recovery\",\"recovery_state\":\"SECRET_SELECTING\","
    283     "\"verbatim_recovery_document\":{}}",
    284     /* unknown field inside a nested object */
    285     "{\"reducer_type\":\"backup\",\"backup_state\":\"CONTINENT_SELECTING\","
    286     "\"continents\":[{\"name\":\"Europe\",\"bogus\":1}]}",
    287     /* unknown provider status */
    288     "{\"reducer_type\":\"backup\",\"backup_state\":\"CONTINENT_SELECTING\","
    289     "\"authentication_providers\":{\"http://x/\":{\"status\":\"weird\"}}}",
    290     /* unknown challenge feedback variant */
    291     "{\"reducer_type\":\"recovery\",\"recovery_state\":\"CHALLENGE_SOLVING\","
    292     "\"challenge_feedback\":{\"3AHRZ6DMV29GQY5CKT18FPX4BJS07ENW3AHRZ6DMV29GQ"
    293     "Y5CKT18\":{\"state\":\"invented\"}}}",
    294     /* bogus state name */
    295     "{\"reducer_type\":\"backup\",\"backup_state\":\"NOT_A_STATE\"}",
    296     /* neither backup nor recovery nor error */
    297     "{\"reducer_type\":\"sideways\"}",
    298     NULL
    299   };
    300 
    301   for (unsigned int i = 0; NULL != bad[i]; i++)
    302   {
    303     json_error_t err;
    304     json_t *json;
    305 
    306     json = json_loads (bad[i],
    307                        JSON_REJECT_DUPLICATES,
    308                        &err);
    309     GNUNET_assert (NULL != json);
    310     check_rejected (bad[i],
    311                     json);
    312     json_decref (json);
    313   }
    314 }
    315 
    316 
    317 /**
    318  * Check that an `error` state survives the codec.
    319  */
    320 static void
    321 test_error_state (void)
    322 {
    323   json_t *json;
    324 
    325   json = GNUNET_JSON_PACK (
    326     GNUNET_JSON_pack_string ("reducer_type",
    327                              "error"),
    328     GNUNET_JSON_pack_uint64 ("code",
    329                              TALER_EC_ANASTASIS_REDUCER_STATE_INVALID),
    330     GNUNET_JSON_pack_string ("hint",
    331                              "a hint"),
    332     GNUNET_JSON_pack_string ("detail",
    333                              "a detail"));
    334   check_idempotent ("error state",
    335                     json);
    336   json_decref (json);
    337 }
    338 
    339 
    340 int
    341 main (int argc,
    342       char *const *argv)
    343 {
    344   const char *srcdir;
    345 
    346   (void) argc;
    347   GNUNET_log_setup ("test-redux-codec",
    348                     "WARNING",
    349                     NULL);
    350   srcdir = getenv ("ANASTASIS_CLI_SRCDIR");
    351   if (NULL == srcdir)
    352   {
    353     fprintf (stderr,
    354              "ANASTASIS_CLI_SRCDIR not set, skipping\n");
    355     return 77;
    356   }
    357   test_fixtures (srcdir);
    358   test_challenge_feedback ();
    359   test_strictness ();
    360   test_error_state ();
    361   return global_ret;
    362 }
    363 
    364 
    365 /* end of test_redux_codec.c */