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_reserve_history.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `reserve_history`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_get_reserve_history().
23 : *
24 : * `reserve_history` is an index: AFTER INSERT triggers on the tables that
25 : * change a reserve add one row each, naming the table and the row. The
26 : * function walks that index newest-first and looks each entry up in the
27 : * table it names. So the checks build a reserve with several kinds of
28 : * operation on it and read the history back, and they check the ETag
29 : * short-circuit -- which must not leave a transaction open.
30 : */
31 : #include "test_common.h"
32 : #include "exchange-database/get_reserve_history.h"
33 : #include "exchange-database/insert_close_request.h"
34 : #include "exchange-database/start.h"
35 : #include "exchange-database/rollback.h"
36 :
37 :
38 : /**
39 : * Account the checks use.
40 : */
41 : static struct TDB_Account account;
42 :
43 :
44 : /**
45 : * Count the entries of @a rh, and how many of them have type @a type.
46 : *
47 : * @param rh history to walk
48 : * @param type operation to count
49 : * @param[out] matched set to the number of entries of that type
50 : * @return total number of entries
51 : */
52 : static unsigned int
53 3 : count_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh,
54 : enum TALER_EXCHANGEDB_ReserveOperation type,
55 : unsigned int *matched)
56 : {
57 3 : unsigned int total = 0;
58 :
59 3 : *matched = 0;
60 3 : for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh;
61 7 : NULL != p;
62 4 : p = p->next)
63 : {
64 4 : total++;
65 4 : if (type == p->type)
66 3 : (*matched)++;
67 : }
68 3 : return total;
69 : }
70 :
71 :
72 : /**
73 : * An unknown reserve has no history at all.
74 : *
75 : * @param pg the database context
76 : * @return 0 on success
77 : */
78 : static int
79 1 : check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
80 : {
81 : struct TALER_ReservePublicKeyP reserve_pub;
82 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
83 : struct TALER_Amount balance;
84 1 : uint64_t etag_out = 42;
85 :
86 1 : TDB_FILL (reserve_pub,
87 : 1);
88 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
89 : TALER_EXCHANGEDB_get_reserve_history (pg,
90 : &reserve_pub,
91 : 0,
92 : 0,
93 : &etag_out,
94 : &balance,
95 : &rh));
96 1 : FAILIF (NULL != rh);
97 1 : return 0;
98 : }
99 :
100 :
101 : /**
102 : * A reserve that exists but was never touched has no history either: the
103 : * `reserves` row alone does not create an index entry.
104 : *
105 : * @param pg the database context
106 : * @return 0 on success
107 : */
108 : static int
109 1 : check_untouched_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
110 : {
111 : struct TALER_ReservePublicKeyP reserve_pub;
112 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
113 : struct TALER_Amount balance;
114 1 : uint64_t etag_out = 42;
115 :
116 1 : TDB_reserve (pg,
117 : 2,
118 : "5",
119 : &reserve_pub);
120 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
121 : TALER_EXCHANGEDB_get_reserve_history (pg,
122 : &reserve_pub,
123 : 0,
124 : 0,
125 : &etag_out,
126 : &balance,
127 : &rh));
128 1 : FAILIF (NULL != rh);
129 1 : return 0;
130 : }
131 :
132 :
133 : /**
134 : * A bank transfer into the reserve shows up as one history entry.
135 : *
136 : * @param pg the database context
137 : * @return 0 on success
138 : */
139 : static int
140 1 : check_bank_transfer (struct TALER_EXCHANGEDB_PostgresContext *pg)
141 : {
142 : struct TALER_ReservePublicKeyP reserve_pub;
143 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
144 : struct TALER_Amount balance;
145 1 : struct TALER_Amount expect = TDB_amount ("10");
146 : unsigned int matched;
147 1 : uint64_t etag_out = 0;
148 :
149 1 : TDB_account (pg,
150 : 10,
151 : &account);
152 1 : TDB_reserve_in (pg,
153 : &account,
154 : 10,
155 : "10",
156 : &reserve_pub);
157 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
158 : TALER_EXCHANGEDB_get_reserve_history (pg,
159 : &reserve_pub,
160 : 0,
161 : 0,
162 : &etag_out,
163 : &balance,
164 : &rh));
165 1 : FAILIF_C (NULL == rh,
166 : TALER_EXCHANGEDB_free_reserve_history (rh));
167 1 : FAILIF_C (0 == etag_out,
168 : TALER_EXCHANGEDB_free_reserve_history (rh));
169 1 : FAILIF_C (0 != TALER_amount_cmp (&balance,
170 : &expect),
171 : TALER_EXCHANGEDB_free_reserve_history (rh));
172 1 : FAILIF_C (1 != count_history (rh,
173 : TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE,
174 : &matched),
175 : TALER_EXCHANGEDB_free_reserve_history (rh));
176 1 : FAILIF_C (1 != matched,
177 : TALER_EXCHANGEDB_free_reserve_history (rh));
178 1 : FAILIF_C (0 != TALER_amount_cmp (&rh->details.bank->amount,
179 : &expect),
180 : TALER_EXCHANGEDB_free_reserve_history (rh));
181 1 : FAILIF_C (0 != strcmp (rh->details.bank->sender_account_details.full_payto,
182 : account.payto.full_payto),
183 : TALER_EXCHANGEDB_free_reserve_history (rh));
184 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
185 1 : return 0;
186 : }
187 :
188 :
189 : /**
190 : * A close request adds a second entry, and the offset filter hides the
191 : * older one.
192 : *
193 : * @param pg the database context
194 : * @return 0 on success
195 : */
196 : static int
197 1 : check_two_entries (struct TALER_EXCHANGEDB_PostgresContext *pg)
198 : {
199 : struct TALER_ReservePublicKeyP reserve_pub;
200 : struct TALER_ReserveSignatureP reserve_sig;
201 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
202 : struct TALER_Amount balance;
203 1 : struct TALER_Amount close_balance = TDB_amount ("10");
204 1 : struct TALER_Amount close_fee = TDB_amount ("0.5");
205 : unsigned int matched;
206 : uint64_t etag_first;
207 1 : uint64_t etag_out = 0;
208 : uint64_t first_off;
209 :
210 1 : TDB_fill (&reserve_pub,
211 : sizeof (reserve_pub),
212 : 10);
213 1 : TDB_FILL (reserve_sig,
214 : 10);
215 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
216 : TALER_EXCHANGEDB_get_reserve_history (pg,
217 : &reserve_pub,
218 : 0,
219 : 0,
220 : &etag_first,
221 : &balance,
222 : &rh));
223 1 : first_off = rh->history_offset;
224 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
225 1 : rh = NULL;
226 :
227 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
228 : TALER_EXCHANGEDB_insert_close_request (pg,
229 : &reserve_pub,
230 : account.payto,
231 : &reserve_sig,
232 : GNUNET_TIME_timestamp_get (),
233 : &close_balance,
234 : &close_fee));
235 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
236 : TALER_EXCHANGEDB_get_reserve_history (pg,
237 : &reserve_pub,
238 : 0,
239 : 0,
240 : &etag_out,
241 : &balance,
242 : &rh));
243 1 : FAILIF_C (2 != count_history (rh,
244 : TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
245 : &matched),
246 : TALER_EXCHANGEDB_free_reserve_history (rh));
247 1 : FAILIF_C (1 != matched,
248 : TALER_EXCHANGEDB_free_reserve_history (rh));
249 : /* The query runs newest-first but the rows are walked back to front, so
250 : the list that comes out is oldest-first. */
251 1 : FAILIF_C (TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE != rh->type,
252 : TALER_EXCHANGEDB_free_reserve_history (rh));
253 1 : FAILIF_C (TALER_EXCHANGEDB_RO_CLOSE_REQUEST != rh->next->type,
254 : TALER_EXCHANGEDB_free_reserve_history (rh));
255 1 : FAILIF_C (rh->history_offset >= rh->next->history_offset,
256 : TALER_EXCHANGEDB_free_reserve_history (rh));
257 1 : FAILIF_C (etag_out <= etag_first,
258 : TALER_EXCHANGEDB_free_reserve_history (rh));
259 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
260 1 : rh = NULL;
261 :
262 : /* starting past the bank transfer leaves only the close request */
263 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
264 : TALER_EXCHANGEDB_get_reserve_history (pg,
265 : &reserve_pub,
266 : first_off,
267 : 0,
268 : &etag_out,
269 : &balance,
270 : &rh));
271 1 : FAILIF_C (1 != count_history (rh,
272 : TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
273 : &matched),
274 : TALER_EXCHANGEDB_free_reserve_history (rh));
275 1 : FAILIF_C (1 != matched,
276 : TALER_EXCHANGEDB_free_reserve_history (rh));
277 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
278 1 : rh = NULL;
279 :
280 : /* starting past everything leaves an empty history, but the reserve is
281 : still known and the ETag is still reported */
282 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
283 : TALER_EXCHANGEDB_get_reserve_history (pg,
284 : &reserve_pub,
285 : etag_out,
286 : 0,
287 : &etag_out,
288 : &balance,
289 : &rh));
290 1 : FAILIF_C (NULL != rh,
291 : TALER_EXCHANGEDB_free_reserve_history (rh));
292 1 : return 0;
293 : }
294 :
295 :
296 : /**
297 : * Passing the current ETag short-circuits the lookup -- and must not leave
298 : * the transaction it opened behind.
299 : *
300 : * @param pg the database context
301 : * @return 0 on success
302 : */
303 : static int
304 1 : check_etag (struct TALER_EXCHANGEDB_PostgresContext *pg)
305 : {
306 : struct TALER_ReservePublicKeyP reserve_pub;
307 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
308 : struct TALER_Amount balance;
309 1 : uint64_t etag_out = 0;
310 1 : uint64_t etag_again = 0;
311 :
312 1 : TDB_fill (&reserve_pub,
313 : sizeof (reserve_pub),
314 : 10);
315 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
316 : TALER_EXCHANGEDB_get_reserve_history (pg,
317 : &reserve_pub,
318 : 0,
319 : 0,
320 : &etag_out,
321 : &balance,
322 : &rh));
323 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
324 1 : rh = NULL;
325 :
326 : /* the client already has this version */
327 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
328 : TALER_EXCHANGEDB_get_reserve_history (pg,
329 : &reserve_pub,
330 : 0,
331 : etag_out,
332 : &etag_again,
333 : &balance,
334 : &rh));
335 1 : FAILIF (etag_again != etag_out);
336 1 : FAILIF (NULL != rh);
337 :
338 : /* The short-circuit above returns early. If it forgets to close the
339 : read-committed transaction it opened, the connection is parked in
340 : "idle in transaction" -- which the next TALER_EXCHANGEDB_start() would
341 : paper over, since its preflight rolls the stale transaction back and
342 : only logs it. So check the bookkeeping preflight itself looks at. */
343 1 : FAILIF (NULL != pg->transaction_name);
344 1 : FAILIF (GNUNET_OK !=
345 : TALER_EXCHANGEDB_start (pg,
346 : "test-reserve-history-etag"));
347 1 : FAILIF (NULL == pg->transaction_name);
348 1 : TALER_EXCHANGEDB_rollback (pg);
349 1 : FAILIF (NULL != pg->transaction_name);
350 1 : return 0;
351 : }
352 :
353 :
354 : /**
355 : * The checks to run, in order.
356 : */
357 : static const struct TDB_Test tests[] = {
358 : { "reserve-history-unknown-reserve",
359 : &check_unknown_reserve },
360 : { "reserve-history-untouched-reserve",
361 : &check_untouched_reserve },
362 : { "reserve-history-bank-transfer",
363 : &check_bank_transfer },
364 : { "reserve-history-two-entries",
365 : &check_two_entries },
366 : { "reserve-history-etag",
367 : &check_etag },
368 : { NULL, NULL }
369 : };
370 :
371 :
372 : int
373 1 : main (int argc,
374 : char *const *argv)
375 : {
376 : int ret;
377 :
378 1 : ret = TDB_main (argc,
379 : argv,
380 : "test-reserve-history",
381 : "Tests for the exchangedb `reserve_history' table",
382 : tests);
383 1 : TDB_account_free (&account);
384 1 : return ret;
385 : }
386 :
387 :
388 : /* end of test_reserve_history.c */
|