test_aml_staff.c (16670B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2026 Taler Systems SA 4 5 TALER 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 TALER 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 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file exchangedb/test_aml_staff.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `aml_staff` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_insert_aml_officer(), 23 * #TALER_EXCHANGEDB_get_aml_officer(), 24 * #TALER_EXCHANGEDB_get_exists_aml_officer() and 25 * #TALER_EXCHANGEDB_iterate_aml_staff_above_serial_id(). 26 * 27 * `aml_staff` has no foreign keys and is append-only: every status change 28 * is a new row, and the current status is the newest one. A change dated 29 * before the one already on file is refused, which is what makes the 30 * "previous_change" output of the insert worth having. 31 */ 32 #include "test_common.h" 33 #include "exchange-database/get_aml_officer.h" 34 #include "exchange-database/get_exists_aml_officer.h" 35 #include "exchange-database/insert_aml_officer.h" 36 #include "exchange-database/iterate_aml_staff_above_serial_id.h" 37 38 39 /** 40 * Build a timestamp from a number of seconds since the epoch. 41 * 42 * @param secs seconds since the epoch 43 * @return the timestamp 44 */ 45 static struct GNUNET_TIME_Timestamp 46 ts (uint64_t secs) 47 { 48 struct GNUNET_TIME_Absolute abs = { 49 .abs_value_us = secs * 1000LLU * 1000LLU 50 }; 51 52 return GNUNET_TIME_absolute_to_timestamp (abs); 53 } 54 55 56 /** 57 * Closure for #staff_cb(). 58 */ 59 struct StaffContext 60 { 61 /** 62 * How many rows did the callback see? 63 */ 64 unsigned int total; 65 66 /** 67 * Stop after this many rows; 0 for no limit. 68 */ 69 unsigned int stop_after; 70 71 /** 72 * Officer we are looking for, NULL to match nothing. 73 */ 74 const struct TALER_AmlOfficerPublicKeyP *decider_pub; 75 76 /** 77 * How many times did we see them? 78 */ 79 unsigned int matched; 80 81 /** 82 * Active flag of the last matching row. 83 */ 84 bool is_active; 85 86 /** 87 * Read-only flag of the last matching row. 88 */ 89 bool read_only; 90 91 /** 92 * Name of the last matching row, owned by this struct. 93 */ 94 char *name; 95 }; 96 97 98 /** 99 * Callback for #TALER_EXCHANGEDB_iterate_aml_staff_above_serial_id(). 100 * 101 * @param cls a `struct StaffContext *` 102 * @param rowid row of the status change 103 * @param decider_pub the officer 104 * @param master_sig signature affirming the status 105 * @param decider_name name of the officer 106 * @param is_active whether they could act from then on 107 * @param read_only whether their access was read-only 108 * @param last_change when the change took effect 109 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 110 */ 111 static enum GNUNET_GenericReturnValue 112 staff_cb (void *cls, 113 uint64_t rowid, 114 const struct TALER_AmlOfficerPublicKeyP *decider_pub, 115 const struct TALER_MasterSignatureP *master_sig, 116 const char *decider_name, 117 bool is_active, 118 bool read_only, 119 struct GNUNET_TIME_Timestamp last_change) 120 { 121 struct StaffContext *ctx = cls; 122 123 (void) rowid; 124 (void) master_sig; 125 (void) last_change; 126 ctx->total++; 127 if ( (NULL != ctx->decider_pub) && 128 (0 == GNUNET_memcmp (decider_pub, 129 ctx->decider_pub)) ) 130 { 131 ctx->matched++; 132 ctx->is_active = is_active; 133 ctx->read_only = read_only; 134 GNUNET_free (ctx->name); 135 ctx->name = GNUNET_strdup (decider_name); 136 } 137 if ( (0 != ctx->stop_after) && 138 (ctx->total >= ctx->stop_after) ) 139 return GNUNET_SYSERR; 140 return GNUNET_OK; 141 } 142 143 144 /** 145 * Record a status change for an officer. 146 * 147 * @param pg the database context 148 * @param seed seed for the officer's key and the master signature 149 * @param name name of the officer 150 * @param is_active whether they may act 151 * @param read_only whether their access is read-only 152 * @param when when the change takes effect, in seconds since the epoch 153 * @param[out] decider_pub set to the officer's key 154 * @param[out] previous_change set to the time of the previous change 155 * @return transaction status 156 */ 157 static enum GNUNET_DB_QueryStatus 158 add_officer (struct TALER_EXCHANGEDB_PostgresContext *pg, 159 uint32_t seed, 160 const char *name, 161 bool is_active, 162 bool read_only, 163 uint64_t when, 164 struct TALER_AmlOfficerPublicKeyP *decider_pub, 165 struct GNUNET_TIME_Timestamp *previous_change) 166 { 167 struct TALER_MasterSignatureP master_sig; 168 169 TDB_fill (decider_pub, 170 sizeof (*decider_pub), 171 seed); 172 TDB_FILL (master_sig, 173 seed); 174 return TALER_EXCHANGEDB_insert_aml_officer (pg, 175 decider_pub, 176 &master_sig, 177 name, 178 is_active, 179 read_only, 180 ts (when), 181 previous_change); 182 } 183 184 185 /** 186 * Nothing is known about an officer who was never appointed. 187 * 188 * @param pg the database context 189 * @return 0 on success 190 */ 191 static int 192 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 193 { 194 struct TALER_AmlOfficerPublicKeyP decider_pub; 195 struct TALER_MasterSignatureP master_sig; 196 struct GNUNET_TIME_Absolute last_change; 197 struct StaffContext ctx = { 0 }; 198 char *decider_name = NULL; 199 bool is_active; 200 bool read_only; 201 202 TDB_FILL (decider_pub, 203 1); 204 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 205 TALER_EXCHANGEDB_get_aml_officer (pg, 206 &decider_pub, 207 &master_sig, 208 &decider_name, 209 &is_active, 210 &read_only, 211 &last_change)); 212 FAILIF (NULL != decider_name); 213 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 214 TALER_EXCHANGEDB_get_exists_aml_officer (pg, 215 &decider_pub, 216 &read_only)); 217 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 218 TALER_EXCHANGEDB_iterate_aml_staff_above_serial_id (pg, 219 0, 220 &staff_cb, 221 &ctx)); 222 FAILIF (0 != ctx.total); 223 return 0; 224 } 225 226 227 /** 228 * Appointing an officer records them and makes them active. 229 * 230 * @param pg the database context 231 * @return 0 on success 232 */ 233 static int 234 check_appoint (struct TALER_EXCHANGEDB_PostgresContext *pg) 235 { 236 struct TALER_AmlOfficerPublicKeyP decider_pub; 237 struct TALER_MasterSignatureP master_sig; 238 struct TALER_MasterSignatureP expect_sig; 239 struct GNUNET_TIME_Timestamp previous_change; 240 struct GNUNET_TIME_Absolute last_change; 241 struct StaffContext ctx; 242 char *decider_name = NULL; 243 bool is_active = false; 244 bool read_only = true; 245 246 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 247 add_officer (pg, 248 10, 249 "Alex Officer", 250 true, 251 false, 252 1600000000, 253 &decider_pub, 254 &previous_change)); 255 FAILIF (1 != TDB_count (pg, 256 "FROM aml_staff")); 257 258 TDB_FILL (expect_sig, 259 10); 260 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 261 TALER_EXCHANGEDB_get_aml_officer (pg, 262 &decider_pub, 263 &master_sig, 264 &decider_name, 265 &is_active, 266 &read_only, 267 &last_change)); 268 FAILIF_C (0 != strcmp (decider_name, 269 "Alex Officer"), 270 GNUNET_free (decider_name)); 271 GNUNET_free (decider_name); 272 FAILIF (0 != GNUNET_memcmp (&master_sig, 273 &expect_sig)); 274 FAILIF (! is_active); 275 FAILIF (read_only); 276 FAILIF (last_change.abs_value_us != 277 ts (1600000000).abs_time.abs_value_us); 278 279 read_only = true; 280 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 281 TALER_EXCHANGEDB_get_exists_aml_officer (pg, 282 &decider_pub, 283 &read_only)); 284 FAILIF (read_only); 285 286 memset (&ctx, 287 0, 288 sizeof (ctx)); 289 ctx.decider_pub = &decider_pub; 290 FAILIF (0 >= 291 TALER_EXCHANGEDB_iterate_aml_staff_above_serial_id (pg, 292 0, 293 &staff_cb, 294 &ctx)); 295 FAILIF_C (1 != ctx.matched, 296 GNUNET_free (ctx.name)); 297 FAILIF_C (0 != strcmp (ctx.name, 298 "Alex Officer"), 299 GNUNET_free (ctx.name)); 300 GNUNET_free (ctx.name); 301 return 0; 302 } 303 304 305 /** 306 * A later change supersedes the earlier one; an earlier one is refused. 307 * 308 * @param pg the database context 309 * @return 0 on success 310 */ 311 static int 312 check_history (struct TALER_EXCHANGEDB_PostgresContext *pg) 313 { 314 struct TALER_AmlOfficerPublicKeyP decider_pub; 315 struct TALER_MasterSignatureP master_sig; 316 struct GNUNET_TIME_Timestamp previous_change; 317 struct GNUNET_TIME_Absolute last_change; 318 struct StaffContext ctx; 319 char *decider_name = NULL; 320 bool is_active = true; 321 bool read_only = false; 322 323 /* the officer is demoted to read-only */ 324 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 325 add_officer (pg, 326 10, 327 "Alex Officer", 328 true, 329 true, 330 1600003600, 331 &decider_pub, 332 &previous_change)); 333 FAILIF (GNUNET_TIME_timestamp_cmp (previous_change, 334 !=, 335 ts (1600000000))); 336 FAILIF (2 != TDB_count (pg, 337 "FROM aml_staff")); 338 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 339 TALER_EXCHANGEDB_get_aml_officer (pg, 340 &decider_pub, 341 &master_sig, 342 &decider_name, 343 &is_active, 344 &read_only, 345 &last_change)); 346 GNUNET_free (decider_name); 347 FAILIF (! read_only); 348 read_only = false; 349 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 350 TALER_EXCHANGEDB_get_exists_aml_officer (pg, 351 &decider_pub, 352 &read_only)); 353 FAILIF (! read_only); 354 355 /* A change dated before the current status is refused. The call still 356 reports ONE_RESULT -- the stored procedure always returns its row -- 357 and the refusal shows in @e previous_change, which comes back as the 358 status that stayed in force rather than as the one being replaced. */ 359 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 360 add_officer (pg, 361 10, 362 "Alex Officer", 363 true, 364 false, 365 1600001800, 366 &decider_pub, 367 &previous_change)); 368 FAILIF (GNUNET_TIME_timestamp_cmp (previous_change, 369 !=, 370 ts (1600003600))); 371 FAILIF (2 != TDB_count (pg, 372 "FROM aml_staff")); 373 374 /* both rows are that officer's history */ 375 memset (&ctx, 376 0, 377 sizeof (ctx)); 378 ctx.decider_pub = &decider_pub; 379 FAILIF (2 != 380 TALER_EXCHANGEDB_iterate_aml_staff_above_serial_id (pg, 381 0, 382 &staff_cb, 383 &ctx)); 384 FAILIF_C (2 != ctx.matched, 385 GNUNET_free (ctx.name)); 386 GNUNET_free (ctx.name); 387 return 0; 388 } 389 390 391 /** 392 * A deactivated officer is no longer active. 393 * 394 * @param pg the database context 395 * @return 0 on success 396 */ 397 static int 398 check_deactivate (struct TALER_EXCHANGEDB_PostgresContext *pg) 399 { 400 struct TALER_AmlOfficerPublicKeyP decider_pub; 401 struct GNUNET_TIME_Timestamp previous_change; 402 bool read_only = false; 403 404 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 405 add_officer (pg, 406 10, 407 "Alex Officer", 408 false, 409 false, 410 1600007200, 411 &decider_pub, 412 &previous_change)); 413 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 414 TALER_EXCHANGEDB_get_exists_aml_officer (pg, 415 &decider_pub, 416 &read_only)); 417 FAILIF (3 != TDB_count (pg, 418 "FROM aml_staff")); 419 return 0; 420 } 421 422 423 /** 424 * The iterator's serial bound and abort return behave as documented. 425 * 426 * @param pg the database context 427 * @return 0 on success 428 */ 429 static int 430 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 431 { 432 struct TALER_AmlOfficerPublicKeyP other; 433 struct GNUNET_TIME_Timestamp previous_change; 434 struct StaffContext ctx; 435 436 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 437 add_officer (pg, 438 11, 439 "Bo Officer", 440 true, 441 true, 442 1600000000, 443 &other, 444 &previous_change)); 445 memset (&ctx, 446 0, 447 sizeof (ctx)); 448 ctx.decider_pub = &other; 449 FAILIF (4 != 450 TALER_EXCHANGEDB_iterate_aml_staff_above_serial_id (pg, 451 0, 452 &staff_cb, 453 &ctx)); 454 FAILIF_C (1 != ctx.matched, 455 GNUNET_free (ctx.name)); 456 FAILIF_C (! ctx.read_only, 457 GNUNET_free (ctx.name)); 458 GNUNET_free (ctx.name); 459 460 memset (&ctx, 461 0, 462 sizeof (ctx)); 463 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 464 TALER_EXCHANGEDB_iterate_aml_staff_above_serial_id (pg, 465 1000, 466 &staff_cb, 467 &ctx)); 468 FAILIF (0 != ctx.total); 469 470 /* A callback that gives up stops the iteration. Unlike most of the 471 other iterators, this one reports how many rows the callback was 472 actually shown rather than how many the query matched. */ 473 memset (&ctx, 474 0, 475 sizeof (ctx)); 476 ctx.stop_after = 1; 477 FAILIF (1 != 478 TALER_EXCHANGEDB_iterate_aml_staff_above_serial_id (pg, 479 0, 480 &staff_cb, 481 &ctx)); 482 FAILIF (1 != ctx.total); 483 return 0; 484 } 485 486 487 /** 488 * The checks to run, in order. 489 */ 490 static const struct TDB_Test tests[] = { 491 { "aml-staff-empty", 492 &check_empty }, 493 { "aml-staff-appoint", 494 &check_appoint }, 495 { "aml-staff-history", 496 &check_history }, 497 { "aml-staff-deactivate", 498 &check_deactivate }, 499 { "aml-staff-iterate", 500 &check_iterate }, 501 { NULL, NULL } 502 }; 503 504 505 int 506 main (int argc, 507 char *const *argv) 508 { 509 return TDB_main (argc, 510 argv, 511 "test-aml-staff", 512 "Tests for the exchangedb `aml_staff' table", 513 tests); 514 } 515 516 517 /* end of test_aml_staff.c */