Line data Source code
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_replication.c
18 : * @brief tests for the exchangedb replication functions
19 : * @author Christian Grothoff
20 : *
21 : * Covers #TALER_EXCHANGEDB_get_serial_by_table(),
22 : * #TALER_EXCHANGEDB_iterate_records_by_table() and
23 : * #TALER_EXCHANGEDB_insert_records_by_table().
24 : *
25 : * These three have no table of their own: each is one big switch over
26 : * #TALER_EXCHANGEDB_ReplicatedTable, and taler-auditor-sync drives them in
27 : * a loop over every value of that enum. A table that is missing from one
28 : * of the switches, or whose SQL no longer matches the schema, breaks
29 : * replication for that table only -- and nothing else in the test suite
30 : * would notice. So the checks here walk the whole enum, and then do one
31 : * full round trip (read a record out, delete it, put it back) to show that
32 : * the reader and the writer agree.
33 : */
34 : #include "test_common.h"
35 : #include "exchange-database/get_serial_by_table.h"
36 : #include "exchange-database/insert_records_by_table.h"
37 : #include "exchange-database/iterate_records_by_table.h"
38 :
39 :
40 : /**
41 : * The two accounts we replicate.
42 : */
43 : static struct TDB_Account account[2];
44 :
45 :
46 : /**
47 : * Denomination the checks use.
48 : */
49 : static struct TDB_Denom denom;
50 :
51 :
52 : /**
53 : * Value we gave @e denom.
54 : */
55 : #define DENOM_VALUE "5"
56 :
57 :
58 : /**
59 : * Closure for #record_cb().
60 : */
61 : struct RecordContext
62 : {
63 : /**
64 : * How many records did the callback see?
65 : */
66 : unsigned int total;
67 :
68 : /**
69 : * Table we expect the records to be from.
70 : */
71 : enum TALER_EXCHANGEDB_ReplicatedTable table;
72 :
73 : /**
74 : * Set to true if a record from another table arrived.
75 : */
76 : bool wrong_table;
77 :
78 : /**
79 : * Serial of the last record.
80 : */
81 : uint64_t serial;
82 :
83 : /**
84 : * Payto URI we are looking for, NULL to look for none.
85 : */
86 : const char *payto;
87 :
88 : /**
89 : * Serial the record with @e payto had, 0 if it did not show up.
90 : */
91 : uint64_t payto_serial;
92 :
93 : /**
94 : * Value of the last denomination record.
95 : */
96 : struct TALER_Amount coin;
97 : };
98 :
99 :
100 : /**
101 : * Callback for #TALER_EXCHANGEDB_iterate_records_by_table().
102 : *
103 : * @param cls a `struct RecordContext *`
104 : * @param td the record
105 : * @return #GNUNET_OK to continue iterating
106 : */
107 : static int
108 8 : record_cb (void *cls,
109 : const struct TALER_EXCHANGEDB_TableData *td)
110 : {
111 8 : struct RecordContext *ctx = cls;
112 :
113 8 : ctx->total++;
114 8 : ctx->serial = td->serial;
115 8 : if (td->table != ctx->table)
116 0 : ctx->wrong_table = true;
117 8 : switch (td->table)
118 : {
119 1 : case TALER_EXCHANGEDB_RT_DENOMINATIONS:
120 1 : ctx->coin = td->details.denominations.coin;
121 1 : break;
122 7 : case TALER_EXCHANGEDB_RT_WIRE_TARGETS:
123 7 : if ( (NULL != ctx->payto) &&
124 6 : (0 == strcmp (ctx->payto,
125 6 : td->details.wire_targets.full_payto_uri.full_payto)) )
126 3 : ctx->payto_serial = td->serial;
127 7 : break;
128 0 : default:
129 0 : break;
130 : }
131 8 : return GNUNET_OK;
132 : }
133 :
134 :
135 : /**
136 : * Every replicated table must have a serial statement, and on an empty
137 : * database none of them has a row.
138 : *
139 : * @param pg the database context
140 : * @return 0 on success
141 : */
142 : static int
143 1 : check_all_serials (struct TALER_EXCHANGEDB_PostgresContext *pg)
144 : {
145 1 : for (unsigned int t = 0;
146 48 : t <= TALER_EXCHANGEDB_RT_KYCAUTHS_IN;
147 47 : t++)
148 : {
149 47 : uint64_t serial = UINT64_MAX;
150 : enum GNUNET_DB_QueryStatus qs;
151 :
152 47 : qs = TALER_EXCHANGEDB_get_serial_by_table (
153 : pg,
154 : (enum TALER_EXCHANGEDB_ReplicatedTable) t,
155 : &serial);
156 47 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs)
157 0 : fprintf (stderr,
158 : "get_serial_by_table(%u) returned %d\n",
159 : t,
160 : (int) qs);
161 47 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs);
162 : }
163 1 : return 0;
164 : }
165 :
166 :
167 : /**
168 : * Every replicated table must be readable, and on an empty database none
169 : * of them yields a record.
170 : *
171 : * @param pg the database context
172 : * @return 0 on success
173 : */
174 : static int
175 1 : check_all_iterable (struct TALER_EXCHANGEDB_PostgresContext *pg)
176 : {
177 1 : for (unsigned int t = 0;
178 48 : t <= TALER_EXCHANGEDB_RT_KYCAUTHS_IN;
179 47 : t++)
180 : {
181 47 : struct RecordContext ctx = {
182 : .table = (enum TALER_EXCHANGEDB_ReplicatedTable) t
183 : };
184 : enum GNUNET_DB_QueryStatus qs;
185 :
186 47 : qs = TALER_EXCHANGEDB_iterate_records_by_table (
187 : pg,
188 : (enum TALER_EXCHANGEDB_ReplicatedTable) t,
189 : 0,
190 : &record_cb,
191 : &ctx);
192 47 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs)
193 0 : fprintf (stderr,
194 : "iterate_records_by_table(%u) returned %d\n",
195 : t,
196 : (int) qs);
197 47 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs);
198 47 : FAILIF (0 != ctx.total);
199 : }
200 1 : return 0;
201 : }
202 :
203 :
204 : /**
205 : * A table outside the enum is refused by all three functions.
206 : *
207 : * @param pg the database context
208 : * @return 0 on success
209 : */
210 : static int
211 1 : check_unknown_table (struct TALER_EXCHANGEDB_PostgresContext *pg)
212 : {
213 1 : enum TALER_EXCHANGEDB_ReplicatedTable bogus
214 : = (enum TALER_EXCHANGEDB_ReplicatedTable) (TALER_EXCHANGEDB_RT_KYCAUTHS_IN
215 : + 1);
216 1 : struct TALER_EXCHANGEDB_TableData td = {
217 : .table = bogus
218 : };
219 1 : struct RecordContext ctx = {
220 : .table = bogus
221 : };
222 : uint64_t serial;
223 :
224 1 : FAILIF (GNUNET_DB_STATUS_HARD_ERROR !=
225 : TALER_EXCHANGEDB_get_serial_by_table (pg,
226 : bogus,
227 : &serial));
228 1 : FAILIF (GNUNET_DB_STATUS_HARD_ERROR !=
229 : TALER_EXCHANGEDB_iterate_records_by_table (pg,
230 : bogus,
231 : 0,
232 : &record_cb,
233 : &ctx));
234 1 : FAILIF (0 != ctx.total);
235 1 : FAILIF (GNUNET_DB_STATUS_HARD_ERROR !=
236 : TALER_EXCHANGEDB_insert_records_by_table (pg,
237 : &td));
238 1 : return 0;
239 : }
240 :
241 :
242 : /**
243 : * With rows in place, the serial of a table is the row ID of its last
244 : * record.
245 : *
246 : * @param pg the database context
247 : * @return 0 on success
248 : */
249 : static int
250 1 : check_serial (struct TALER_EXCHANGEDB_PostgresContext *pg)
251 : {
252 1 : uint64_t serial = 0;
253 :
254 3 : for (unsigned int i = 0; i < 2; i++)
255 2 : TDB_account (pg,
256 : 10 + i,
257 : &account[i]);
258 1 : TDB_denom (pg,
259 : 10,
260 : DENOM_VALUE,
261 : "0.1",
262 : &denom);
263 :
264 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
265 : TALER_EXCHANGEDB_get_serial_by_table (
266 : pg,
267 : TALER_EXCHANGEDB_RT_WIRE_TARGETS,
268 : &serial));
269 1 : FAILIF (2 != serial);
270 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
271 : TALER_EXCHANGEDB_get_serial_by_table (
272 : pg,
273 : TALER_EXCHANGEDB_RT_KYC_TARGETS,
274 : &serial));
275 1 : FAILIF (2 != serial);
276 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
277 : TALER_EXCHANGEDB_get_serial_by_table (
278 : pg,
279 : TALER_EXCHANGEDB_RT_DENOMINATIONS,
280 : &serial));
281 1 : FAILIF (serial != denom.serial);
282 :
283 : /* a table nobody wrote to still has no serial */
284 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
285 : TALER_EXCHANGEDB_get_serial_by_table (
286 : pg,
287 : TALER_EXCHANGEDB_RT_WIRE_OUT,
288 : &serial));
289 1 : return 0;
290 : }
291 :
292 :
293 : /**
294 : * Reading records returns them in serial order and skips everything up
295 : * to and including the requested serial.
296 : *
297 : * @param pg the database context
298 : * @return 0 on success
299 : */
300 : static int
301 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
302 : {
303 : struct RecordContext ctx;
304 1 : struct TALER_Amount expect = TDB_amount (DENOM_VALUE);
305 :
306 1 : memset (&ctx,
307 : 0,
308 : sizeof (ctx));
309 1 : ctx.table = TALER_EXCHANGEDB_RT_DENOMINATIONS;
310 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
311 : TALER_EXCHANGEDB_iterate_records_by_table (
312 : pg,
313 : TALER_EXCHANGEDB_RT_DENOMINATIONS,
314 : 0,
315 : &record_cb,
316 : &ctx));
317 1 : FAILIF (1 != ctx.total);
318 1 : FAILIF (ctx.wrong_table);
319 1 : FAILIF (ctx.serial != denom.serial);
320 1 : FAILIF (0 != TALER_amount_cmp (&ctx.coin,
321 : &expect));
322 :
323 : /* nothing above the last serial */
324 1 : memset (&ctx,
325 : 0,
326 : sizeof (ctx));
327 1 : ctx.table = TALER_EXCHANGEDB_RT_DENOMINATIONS;
328 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
329 : TALER_EXCHANGEDB_iterate_records_by_table (
330 : pg,
331 : TALER_EXCHANGEDB_RT_DENOMINATIONS,
332 : denom.serial,
333 : &record_cb,
334 : &ctx));
335 1 : FAILIF (0 != ctx.total);
336 :
337 : /* both accounts, and the second one only above the first serial */
338 1 : memset (&ctx,
339 : 0,
340 : sizeof (ctx));
341 1 : ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
342 1 : ctx.payto = account[1].payto.full_payto;
343 1 : FAILIF (0 >=
344 : TALER_EXCHANGEDB_iterate_records_by_table (
345 : pg,
346 : TALER_EXCHANGEDB_RT_WIRE_TARGETS,
347 : 0,
348 : &record_cb,
349 : &ctx));
350 1 : FAILIF (2 != ctx.total);
351 1 : FAILIF (0 == ctx.payto_serial);
352 :
353 1 : memset (&ctx,
354 : 0,
355 : sizeof (ctx));
356 1 : ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
357 1 : FAILIF (0 >=
358 : TALER_EXCHANGEDB_iterate_records_by_table (
359 : pg,
360 : TALER_EXCHANGEDB_RT_WIRE_TARGETS,
361 : 1,
362 : &record_cb,
363 : &ctx));
364 1 : FAILIF (1 != ctx.total);
365 1 : FAILIF (2 != ctx.serial);
366 1 : return 0;
367 : }
368 :
369 :
370 : /**
371 : * A record that is read out, dropped and written back lands in the same
372 : * row it came from.
373 : *
374 : * @param pg the database context
375 : * @return 0 on success
376 : */
377 : static int
378 1 : check_round_trip (struct TALER_EXCHANGEDB_PostgresContext *pg)
379 : {
380 1 : struct TALER_EXCHANGEDB_TableData td = {
381 : .table = TALER_EXCHANGEDB_RT_WIRE_TARGETS
382 : };
383 : struct RecordContext ctx;
384 : char *hex;
385 :
386 : /* find the row of the second account */
387 1 : memset (&ctx,
388 : 0,
389 : sizeof (ctx));
390 1 : ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
391 1 : ctx.payto = account[1].payto.full_payto;
392 1 : FAILIF (0 >=
393 : TALER_EXCHANGEDB_iterate_records_by_table (
394 : pg,
395 : TALER_EXCHANGEDB_RT_WIRE_TARGETS,
396 : 0,
397 : &record_cb,
398 : &ctx));
399 1 : FAILIF (0 == ctx.payto_serial);
400 :
401 : /* the account is not referenced by anything, so we can drop it and
402 : replicate it back in */
403 1 : hex = TDB_hex (&account[1].h_full,
404 : sizeof (account[1].h_full));
405 1 : FAILIF_C (GNUNET_OK !=
406 : TDB_exec (pg,
407 : "DELETE FROM wire_targets"
408 : " WHERE wire_target_h_payto=decode('%s','hex');",
409 : hex),
410 : GNUNET_free (hex));
411 1 : GNUNET_free (hex);
412 1 : FAILIF (1 != TDB_count (pg,
413 : "FROM wire_targets"));
414 :
415 1 : td.serial = ctx.payto_serial;
416 1 : td.details.wire_targets.full_payto_uri = account[1].payto;
417 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
418 : TALER_EXCHANGEDB_insert_records_by_table (pg,
419 : &td));
420 1 : FAILIF (2 != TDB_count (pg,
421 : "FROM wire_targets"));
422 :
423 : /* and it is back under its original serial, with its payto intact */
424 1 : memset (&ctx,
425 : 0,
426 : sizeof (ctx));
427 1 : ctx.table = TALER_EXCHANGEDB_RT_WIRE_TARGETS;
428 1 : ctx.payto = account[1].payto.full_payto;
429 1 : FAILIF (0 >=
430 : TALER_EXCHANGEDB_iterate_records_by_table (
431 : pg,
432 : TALER_EXCHANGEDB_RT_WIRE_TARGETS,
433 : 0,
434 : &record_cb,
435 : &ctx));
436 1 : FAILIF (2 != ctx.total);
437 1 : FAILIF (ctx.payto_serial != td.serial);
438 1 : return 0;
439 : }
440 :
441 :
442 : /**
443 : * The checks to run, in order.
444 : */
445 : static const struct TDB_Test tests[] = {
446 : { "replication-all-serials",
447 : &check_all_serials },
448 : { "replication-all-iterable",
449 : &check_all_iterable },
450 : { "replication-unknown-table",
451 : &check_unknown_table },
452 : { "replication-serial",
453 : &check_serial },
454 : { "replication-iterate",
455 : &check_iterate },
456 : { "replication-round-trip",
457 : &check_round_trip },
458 : { NULL, NULL }
459 : };
460 :
461 :
462 : int
463 1 : main (int argc,
464 : char *const *argv)
465 : {
466 : int ret;
467 :
468 1 : ret = TDB_main (argc,
469 : argv,
470 : "test-replication",
471 : "Tests for the exchangedb replication functions",
472 : tests);
473 1 : TDB_denom_free (&denom);
474 3 : for (unsigned int i = 0; i < 2; i++)
475 2 : TDB_account_free (&account[i]);
476 1 : return ret;
477 : }
478 :
479 :
480 : /* end of test_replication.c */
|