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 */