1#include <unistd.h>
2#include <stdlib.h>
3#include "config.h"
4#include "cunit/cunit.h"
5#include "imap/quota.h"
6#include "xmalloc.h"
7#include "retry.h"
8#include "imap/global.h"
9#include "imap/imap_err.h"
10#include "cyrusdb.h"
11#include "libcyr_cfg.h"
12#include "libconfig.h"
13#include "hash.h"
14
15#define DBDIR                   "test-mb-dbdir"
16#define QUOTAROOT               "user.smurf"
17#define QUOTAROOT_NONEXISTANT   "no-such-quotaroot"
18
19static char *backend = CUNIT_PARAM("quotalegacy,skiplist");
20
21static void test_names(void)
22{
23    int r;
24
25    CU_ASSERT_STRING_EQUAL(quota_names[QUOTA_STORAGE],
26                           "STORAGE");
27    r = quota_name_to_resource("STORAGE");
28    CU_ASSERT_EQUAL(r, QUOTA_STORAGE);
29    r = quota_name_to_resource("storage");
30    CU_ASSERT_EQUAL(r, QUOTA_STORAGE);
31    r = quota_name_to_resource("StOrAge");
32    CU_ASSERT_EQUAL(r, QUOTA_STORAGE);
33
34    CU_ASSERT_STRING_EQUAL(quota_names[QUOTA_ANNOTSTORAGE],
35                           "X-ANNOTATION-STORAGE");
36    r = quota_name_to_resource("X-ANNOTATION-STORAGE");
37    CU_ASSERT_EQUAL(r, QUOTA_ANNOTSTORAGE);
38    r = quota_name_to_resource("x-annotation-storage");
39    CU_ASSERT_EQUAL(r, QUOTA_ANNOTSTORAGE);
40    r = quota_name_to_resource("X-AnNotAtiOn-StOragE");
41    CU_ASSERT_EQUAL(r, QUOTA_ANNOTSTORAGE);
42
43    r = quota_name_to_resource("nonesuch");
44    CU_ASSERT_EQUAL(r, -1);
45
46}
47
48/*
49 * Trying to read quota for a quotaroot which is NULL, an empty string,
50 * or a string which is not found in the database.
51 */
52static void test_read_no_root(void)
53{
54    struct quota q;
55    struct txn *txn = NULL;
56    int r;
57
58    q.root = NULL;
59    r = quota_read(&q, NULL, 0);
60    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
61
62    q.root = "";
63    r = quota_read(&q, NULL, 0);
64    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
65
66    /* Also, for NULL or "" no txn is started */
67    q.root = NULL;
68    r = quota_read(&q, &txn, 1);
69    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
70    CU_ASSERT_PTR_NULL(txn);
71
72    q.root = "";
73    r = quota_read(&q, &txn, 1);
74    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
75    CU_ASSERT_PTR_NULL(txn);
76
77    q.root = QUOTAROOT_NONEXISTANT;
78    r = quota_read(&q, NULL, 0);
79    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
80}
81
82static void test_write_no_root(void)
83{
84    struct quota q;
85    struct txn *txn = NULL;
86    int r;
87
88    q.root = NULL;
89    r = quota_write(&q, NULL);
90    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
91
92    q.root = "";
93    r = quota_write(&q, NULL);
94    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
95
96    /* Also, for NULL or "" no txn is started */
97    q.root = NULL;
98    r = quota_write(&q, &txn);
99    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
100    CU_ASSERT_PTR_NULL(txn);
101
102    q.root = "";
103    r = quota_write(&q, &txn);
104    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
105    CU_ASSERT_PTR_NULL(txn);
106}
107
108
109static void test_read_write(void)
110{
111    struct quota q;
112    struct quota q2;
113    struct txn *txn = NULL;
114    struct txn *oldtxn = NULL;
115    int res;
116    int r;
117
118    memset(&q, 0, sizeof(q));
119    q.root = QUOTAROOT;
120
121    r = quota_read(&q, &txn, 1);
122    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
123    /* we went to the db and started a transaction */
124    CU_ASSERT_PTR_NOT_NULL(txn);
125
126    q.useds[QUOTA_STORAGE] = 12345;
127    q.limits[QUOTA_STORAGE] = 678;
128    q.useds[QUOTA_MESSAGE] = 2345;
129    q.limits[QUOTA_MESSAGE] = 78;
130    q.useds[QUOTA_ANNOTSTORAGE] = 345;
131    q.limits[QUOTA_ANNOTSTORAGE] = 8;
132
133    oldtxn = txn;
134    r = quota_write(&q, &txn);
135    CU_ASSERT_EQUAL(r, 0);
136    CU_ASSERT_PTR_NOT_NULL(txn);
137    CU_ASSERT_PTR_EQUAL(oldtxn, txn);
138
139    /* reading in the same txn gets the new values */
140    memset(&q2, 0, sizeof(q2));
141    q2.root = QUOTAROOT;
142    r = quota_read(&q2, &txn, 0);
143    CU_ASSERT_EQUAL(r, 0);
144    for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
145        CU_ASSERT_EQUAL(q2.useds[res], q.useds[res]);
146        CU_ASSERT_EQUAL(q2.limits[res], q.limits[res]);
147    }
148
149    /* commit the txn */
150    quota_commit(&txn);
151    CU_ASSERT_PTR_NULL(txn);
152
153    /* reading in a new txn gets the new values */
154    memset(&q2, 0, sizeof(q2));
155    q2.root = QUOTAROOT;
156    r = quota_read(&q2, &txn, 0);
157    CU_ASSERT_EQUAL(r, 0);
158    CU_ASSERT_PTR_NOT_NULL(txn);
159    for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
160        CU_ASSERT_EQUAL(q2.useds[res], q.useds[res]);
161        CU_ASSERT_EQUAL(q2.limits[res], q.limits[res]);
162    }
163
164    quota_commit(&txn);
165    CU_ASSERT_PTR_NULL(txn);
166}
167
168static void test_abort(void)
169{
170    struct quota q;
171    struct quota q2;
172    struct txn *txn = NULL;
173    struct txn *oldtxn = NULL;
174    int r;
175
176    memset(&q, 0, sizeof(q));
177    q.root = QUOTAROOT;
178
179    r = quota_read(&q, &txn, 1);
180    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
181    /* we went to the db and started a transaction */
182    CU_ASSERT_PTR_NOT_NULL(txn);
183
184    q.useds[QUOTA_STORAGE] = 12345;
185    q.limits[QUOTA_STORAGE] = 678;
186
187    oldtxn = txn;
188    r = quota_write(&q, &txn);
189    CU_ASSERT_EQUAL(r, 0);
190    CU_ASSERT_PTR_NOT_NULL(txn);
191    CU_ASSERT_PTR_EQUAL(oldtxn, txn);
192
193    /* reading in the same txn gets the new values */
194    memset(&q2, 0, sizeof(q2));
195    q2.root = QUOTAROOT;
196    r = quota_read(&q2, &txn, 0);
197    CU_ASSERT_EQUAL(r, 0);
198    CU_ASSERT_EQUAL(q2.useds[QUOTA_STORAGE], q.useds[QUOTA_STORAGE]);
199    CU_ASSERT_EQUAL(q2.limits[QUOTA_STORAGE], q.limits[QUOTA_STORAGE]);
200
201    /* abort the txn */
202    quota_abort(&txn);
203    CU_ASSERT_PTR_NULL(txn);
204
205    /* reading in a new txn gets no result */
206    memset(&q2, 0, sizeof(q2));
207    q2.root = QUOTAROOT;
208    r = quota_read(&q2, &txn, 0);
209    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
210    CU_ASSERT_PTR_NOT_NULL(txn);
211
212    quota_commit(&txn);
213    CU_ASSERT_PTR_NULL(txn);
214}
215
216static void test_abort2(void)
217{
218    struct quota q;
219    struct quota oldq;
220    struct quota q2;
221    struct txn *txn = NULL;
222    int r;
223
224    memset(&q, 0, sizeof(q));
225    q.root = QUOTAROOT;
226
227    r = quota_read(&q, &txn, 1);
228    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
229    /* we went to the db and started a transaction */
230    CU_ASSERT_PTR_NOT_NULL(txn);
231
232    q.useds[QUOTA_STORAGE] = 12345;
233    q.limits[QUOTA_STORAGE] = 678;
234
235    r = quota_write(&q, &txn);
236    CU_ASSERT_EQUAL(r, 0);
237    CU_ASSERT_PTR_NOT_NULL(txn);
238
239    /* commit some old values */
240    quota_commit(&txn);
241    CU_ASSERT_PTR_NULL(txn);
242
243    /* write some new values */
244    oldq = q;
245    q.useds[QUOTA_STORAGE] = 23456;
246    q.limits[QUOTA_STORAGE] = 789;
247    r = quota_write(&q, &txn);
248    CU_ASSERT_EQUAL(r, 0);
249
250    /* reading in the same txn gets the new values */
251    memset(&q2, 0, sizeof(q2));
252    q2.root = QUOTAROOT;
253    r = quota_read(&q2, &txn, 0);
254    CU_ASSERT_EQUAL(r, 0);
255    CU_ASSERT_EQUAL(q2.useds[QUOTA_STORAGE], q.useds[QUOTA_STORAGE]);
256    CU_ASSERT_EQUAL(q2.limits[QUOTA_STORAGE], q.limits[QUOTA_STORAGE]);
257
258    /* abort the txn */
259    quota_abort(&txn);
260    CU_ASSERT_PTR_NULL(txn);
261
262    /* reading in a new txn gets old values */
263    memset(&q2, 0, sizeof(q2));
264    q2.root = QUOTAROOT;
265    r = quota_read(&q2, &txn, 0);
266    CU_ASSERT_EQUAL(r, 0);
267    CU_ASSERT_PTR_NOT_NULL(txn);
268    CU_ASSERT_EQUAL(q2.useds[QUOTA_STORAGE], oldq.useds[QUOTA_STORAGE]);
269    CU_ASSERT_EQUAL(q2.limits[QUOTA_STORAGE], oldq.limits[QUOTA_STORAGE]);
270
271    quota_commit(&txn);
272    CU_ASSERT_PTR_NULL(txn);
273}
274
275static void test_check_use(void)
276{
277    struct quota q;
278    unsigned int i;
279    int r;
280
281    memset(&q, 0, sizeof(q));
282    q.root = QUOTAROOT;
283    q.limits[QUOTA_STORAGE] = 100;
284
285    for (i = 1 ; i <= 63 ; i++)
286    {
287        quota_t diff = (1ULL<<i)-1;
288        r = quota_check(&q, QUOTA_STORAGE, diff);
289        CU_ASSERT_EQUAL(r, (diff > 100*1024 ? IMAP_QUOTA_EXCEEDED : 0));
290        CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 0);
291    }
292
293    r = quota_check(&q, QUOTA_STORAGE, 10*1024);
294    CU_ASSERT_EQUAL(r, 0);
295    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 0);
296
297    quota_use(&q, QUOTA_STORAGE, 10*1024);
298    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 10*1024);
299
300    r = quota_check(&q, QUOTA_STORAGE, 80*1024);
301    CU_ASSERT_EQUAL(r, 0);
302    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 10*1024);
303
304    quota_use(&q, QUOTA_STORAGE, 80*1024);
305    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], (10+80)*1024);
306
307    r = quota_check(&q, QUOTA_STORAGE, 15*1024);
308    CU_ASSERT_EQUAL(r, IMAP_QUOTA_EXCEEDED);
309    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], (10+80)*1024);
310
311    /* test the zero limit */
312    memset(&q, 0, sizeof(q));
313    q.root = QUOTAROOT;
314    q.limits[QUOTA_STORAGE] = 0;
315
316    r = quota_check(&q, QUOTA_STORAGE, 15*1024);
317    CU_ASSERT_EQUAL(r, IMAP_QUOTA_EXCEEDED);
318    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 0);
319
320    r = quota_check(&q, QUOTA_STORAGE, 1);
321    CU_ASSERT_EQUAL(r, IMAP_QUOTA_EXCEEDED);
322    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 0);
323
324    /* test the special limit QUOTA_UNLIMITED */
325    memset(&q, 0, sizeof(q));
326    q.root = QUOTAROOT;
327    q.limits[QUOTA_STORAGE] = QUOTA_UNLIMITED;
328
329    for (i = 1 ; i <= 63 ; i++)
330    {
331        quota_t diff = (1ULL<<i)-1;
332        r = quota_check(&q, QUOTA_STORAGE, diff);
333        CU_ASSERT_EQUAL(r, 0);
334        CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 0);
335    }
336
337    /* test negative diffs in quota_check */
338    memset(&q, 0, sizeof(q));
339    q.root = QUOTAROOT;
340    q.useds[QUOTA_STORAGE] = 80*1024;   /* used 80 KiB */
341    q.limits[QUOTA_STORAGE] = 100;      /* limit 100 KiB */
342
343    r = quota_check(&q, QUOTA_STORAGE, -1);
344    CU_ASSERT_EQUAL(r, 0);
345    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 80*1024);
346
347    r = quota_check(&q, QUOTA_STORAGE, -10*1024);
348    CU_ASSERT_EQUAL(r, 0);
349    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 80*1024);
350
351    r = quota_check(&q, QUOTA_STORAGE, -80*1024);
352    CU_ASSERT_EQUAL(r, 0);
353    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 80*1024);
354
355    r = quota_check(&q, QUOTA_STORAGE, -1000*1024);
356    CU_ASSERT_EQUAL(r, 0);
357    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 80*1024);
358
359    /* test negative diffs in quota_use */
360    memset(&q, 0, sizeof(q));
361    q.root = QUOTAROOT;
362    q.useds[QUOTA_STORAGE] = 80*1024;   /* used 80 KiB */
363    q.limits[QUOTA_STORAGE] = 100;      /* limit 100 KiB */
364
365    quota_use(&q, QUOTA_STORAGE, -1);
366    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 80*1024-1);
367
368    q.useds[QUOTA_STORAGE] = 80*1024;   /* used 80 KiB */
369    quota_use(&q, QUOTA_STORAGE, -10*1024);
370    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 70*1024);
371
372    q.useds[QUOTA_STORAGE] = 80*1024;   /* used 80 KiB */
373    quota_use(&q, QUOTA_STORAGE, -80*1024);
374    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 0);
375
376    /* test underflow in quota_use */
377    CU_SYSLOG_MATCH("Quota underflow");
378    q.useds[QUOTA_STORAGE] = 80*1024;   /* used 80 KiB */
379    quota_use(&q, QUOTA_STORAGE, -1000*1024);
380    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 0); /* clamped */
381    CU_ASSERT_SYSLOG(/*all*/0, 1);              /* whined */
382}
383
384static void test_check_overquota(void)
385{
386    struct quota q;
387
388    memset(&q, 0, sizeof(q));
389    q.root = QUOTAROOT;
390    q.limits[QUOTA_STORAGE] = 100;
391    q.limits[QUOTA_MESSAGE] = 2;
392
393    /* test not overquota */
394    quota_use(&q, QUOTA_STORAGE, 10*1024);
395    quota_use(&q, QUOTA_MESSAGE, 1);
396    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 10*1024);
397    CU_ASSERT_EQUAL(q.useds[QUOTA_MESSAGE], 1);
398    CU_ASSERT_EQUAL(0, quota_is_overquota(&q, QUOTA_STORAGE, NULL));
399    CU_ASSERT_EQUAL(0, quota_is_overquota(&q, QUOTA_MESSAGE, NULL));
400
401    /* test now overquota */
402    quota_use(&q, QUOTA_STORAGE, 90*1024);
403    quota_use(&q, QUOTA_MESSAGE, 1);
404    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 100*1024);
405    CU_ASSERT_EQUAL(q.useds[QUOTA_MESSAGE], 2);
406    CU_ASSERT_EQUAL(1, quota_is_overquota(&q, QUOTA_STORAGE, NULL));
407    CU_ASSERT_EQUAL(1, quota_is_overquota(&q, QUOTA_MESSAGE, NULL));
408
409    /* test under quota */
410    quota_use(&q, QUOTA_STORAGE, -10*1024);
411    quota_use(&q, QUOTA_MESSAGE, -1);
412    CU_ASSERT_EQUAL(q.useds[QUOTA_STORAGE], 90*1024);
413    CU_ASSERT_EQUAL(q.useds[QUOTA_MESSAGE], 1);
414    CU_ASSERT_EQUAL(0, quota_is_overquota(&q, QUOTA_STORAGE, NULL));
415    CU_ASSERT_EQUAL(0, quota_is_overquota(&q, QUOTA_MESSAGE, NULL));
416}
417
418static void test_update_useds(void)
419{
420    struct quota q;
421    struct quota q2;
422    struct txn *txn = NULL;
423    int res;
424    quota_t quota_diff[QUOTA_NUMRESOURCES];
425    int r;
426
427    memset(&q, 0, sizeof(q));
428    q.root = QUOTAROOT;
429    memset(quota_diff, 0, sizeof(quota_diff));
430
431    /* updating a NULL or empty or non-existant root returns the error */
432    quota_diff[QUOTA_STORAGE] = 10*1024;
433    quota_diff[QUOTA_MESSAGE] = 2;
434    quota_diff[QUOTA_ANNOTSTORAGE] = 1*1024;
435    r = quota_update_useds(NULL, quota_diff, NULL);
436    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
437
438    r = quota_update_useds("", quota_diff, NULL);
439    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
440
441    r = quota_update_useds(QUOTAROOT_NONEXISTANT, quota_diff, NULL);
442    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
443
444    /* set limits */
445    q.limits[QUOTA_STORAGE] = 100;  /* limit storage to 100 KiB */
446    q.limits[QUOTA_MESSAGE] = 20;  /* limit messages to 20 */
447    q.limits[QUOTA_ANNOTSTORAGE] = 10;  /* limit annotations storage to 10 KiB */
448    r = quota_write(&q, &txn);
449    CU_ASSERT_EQUAL(r, 0);
450    CU_ASSERT_PTR_NOT_NULL(txn);
451    quota_commit(&txn);
452    CU_ASSERT_PTR_NULL(txn);
453
454#define TESTCASE(d0, d1, d2, e0, e1, e2) { \
455    static const quota_t diff[QUOTA_NUMRESOURCES] = { d0, d1, d2 }; \
456    static const quota_t expused[QUOTA_NUMRESOURCES] = { e0, e1, e2 }; \
457    r = quota_update_useds(QUOTAROOT, diff, NULL); \
458    CU_ASSERT_EQUAL(r, 0); \
459    memset(&q2, 0, sizeof(q2)); \
460    q2.root = QUOTAROOT; \
461    r = quota_read(&q2, NULL, 0); \
462    CU_ASSERT_EQUAL(r, 0); \
463    for (res = 0; res < QUOTA_NUMRESOURCES; res++) { \
464        CU_ASSERT_EQUAL(q2.useds[res], expused[res]); \
465        CU_ASSERT_EQUAL(q2.limits[res], q.limits[res]) \
466    } \
467}
468
469    /* updating a root which has a record, succeeds */
470    TESTCASE(10*1024, 2, 1*1024,
471             10*1024, 2, 1*1024);
472
473    /* updating some more adds to the used value */
474    TESTCASE(80*1024, 16, 8*1024,
475             90*1024, 18, 9*1024);
476
477    /* updating with a zero diff does not change the used value */
478    TESTCASE(0, 0, 0,
479             90*1024, 18, 9*1024);
480
481    /* quota_update_useds() does not enforce the limit */
482    TESTCASE(20*1024, 4, 2*1024,
483             110*1024,          /* used 110 KiB limit 100 KiB */
484             22,                /* used 22 limit 20 */
485             11*1024);          /* used 11 KiB limit 10 KiB */
486
487    /* updating with a negative value */
488    TESTCASE(-70*1024, -14, -7*1024,
489             40*1024, 8, 4*1024);
490
491    /* underflow is prevented */
492    TESTCASE(-50*1024, -10, -5*1024,
493             0, 0, 0);
494
495    /* XXX we call quota_update_useds() with a NULL mailbox, which
496     * XXX will crash in some circumstances (see comment inline), but
497     * XXX our tests don't crash... which means we're missing tests
498     * XXX for the codepath that depends on mboxname!
499     * XXX https://github.com/cyrusimap/cyrus-imapd/issues/2808
500     */
501
502#undef TESTCASE
503}
504
505static void test_delete(void)
506{
507    struct quota q;
508    struct quota q2;
509    struct txn *txn = NULL;
510    int r;
511
512    /* first, deleteroot() behaviour with nothing in the db */
513    r = quota_deleteroot(NULL);
514    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
515
516    r = quota_deleteroot("");
517    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
518
519    r = quota_deleteroot(QUOTAROOT_NONEXISTANT);
520    CU_ASSERT_EQUAL(r, 0);
521
522    r = quota_deleteroot(QUOTAROOT);
523    CU_ASSERT_EQUAL(r, 0);
524
525
526    /* add a record to the db and check it's there */
527    memset(&q, 0, sizeof(q));
528    q.root = QUOTAROOT;
529    q.useds[QUOTA_STORAGE] = 12345;
530    q.limits[QUOTA_STORAGE] = 678;
531
532    r = quota_write(&q, &txn);
533    CU_ASSERT_EQUAL(r, 0);
534    CU_ASSERT_PTR_NOT_NULL(txn);
535
536    quota_commit(&txn);
537    CU_ASSERT_PTR_NULL(txn);
538
539    memset(&q2, 0, sizeof(q2));
540    q2.root = QUOTAROOT;
541    r = quota_read(&q2, NULL, 0);
542    CU_ASSERT_EQUAL(r, 0);
543    CU_ASSERT_EQUAL(q2.useds[QUOTA_STORAGE], q.useds[QUOTA_STORAGE]);
544    CU_ASSERT_EQUAL(q2.limits[QUOTA_STORAGE], q.limits[QUOTA_STORAGE]);
545
546    /* check behaviour with a record in the db */
547    r = quota_deleteroot(NULL);
548    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
549
550    r = quota_deleteroot("");
551    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
552
553    r = quota_deleteroot(QUOTAROOT_NONEXISTANT);
554    CU_ASSERT_EQUAL(r, 0);
555
556    r = quota_deleteroot(QUOTAROOT);
557    CU_ASSERT_EQUAL(r, 0);
558
559    /* record should now be gone */
560    memset(&q2, 0, sizeof(q2));
561    q2.root = QUOTAROOT;
562    r = quota_read(&q2, NULL, 0);
563    CU_ASSERT_EQUAL(r, IMAP_QUOTAROOT_NONEXISTENT);
564}
565
566#if 0
567static void count_cb(const char *key __attribute__((unused)),
568                     void *data __attribute__((unused)),
569                     void *rock)
570{
571    unsigned int *np = (unsigned int *)rock;
572    (*np)++;
573}
574
575static unsigned int hash_count(hash_table *ht)
576{
577    unsigned int n = 0;
578    hash_enumerate(ht, count_cb, &n);
579    return n;
580}
581
582static const char *nth_quotaroot(unsigned int n)
583{
584    static char buf[100];
585    static const char * const ones[10] = {
586        ".around", ".defend", ".failure", ".develop", ".nader",
587        ".fuels", ".mixtec", ".bunting", ".energy", ".orlons" };
588    static const char * const tens[10] = {
589        "", ".blob", ".flakier", ".freda", ".garbs",
590        ".debug", ".ava", ".dumbing", ".addend", ".apaches" };
591    static const char * const hundreds[10] = {
592        "", ".volcker", ".genies", ".spiro", ".alone",
593        ".drawer", ".eighth", ".micheal", ".coheres", ".garrick" };
594    static const char * const thousands[10] = {
595        "", ".epilogs", ".cue", ".cahoots", ".decking",
596        ".gypsum", ".gratis", ".dimple", ".pedro", ".fading" };
597
598    snprintf(buf, sizeof(buf), "user%s%s%s%s",
599             thousands[(n / 1000) % 10],
600             hundreds[(n / 100) % 10],
601             tens[(n / 10) % 10],
602             ones[n % 10]);
603    return buf;
604}
605
606static quota_t nth_used(unsigned int n)
607{
608    quota_t u = n;
609
610    if (n % 7 == 0)
611        u *= 1023;
612    else if (n % 17 == 0)
613        u |= (u << 53);
614    return u;
615}
616
617static int nth_limit(unsigned int n)
618{
619    return n * 1024 * 1023;
620}
621
622static int found_cb(struct quota *q, void *rock)
623{
624    hash_table *exphash = (hash_table *)rock;
625    struct quota *expected = hash_lookup(q->root, exphash);
626
627    CU_ASSERT_PTR_NOT_NULL(expected);
628    CU_ASSERT_STRING_EQUAL(q->root, expected->root);
629    CU_ASSERT_EQUAL(q->useds[QUOTA_STORAGE],
630                    expected->useds[QUOTA_STORAGE]);
631    CU_ASSERT_EQUAL(q->limits[QUOTA_STORAGE],
632                    expected->limits[QUOTA_STORAGE]);
633
634    hash_del(q->root, exphash);
635    return 0;
636}
637
638#define FOREACH_PRECONDITION(condition, expcount) \
639{ \
640    unsigned int nsubset = 0; \
641    unsigned int i; \
642    for (i = 0 ; i <= MAXN ; i++) { \
643        if ((condition)) { \
644            hash_insert(expected[i].root, &expected[i], &exphash); \
645            nsubset++; \
646        } \
647    } \
648    CU_ASSERT_EQUAL(nsubset, expcount); \
649    CU_ASSERT_EQUAL(hash_count(&exphash), nsubset); \
650}
651
652#define FOREACH_POSTCONDITION() \
653    CU_ASSERT_EQUAL(r, 0); \
654    CU_ASSERT_EQUAL(hash_count(&exphash), 0);
655
656#define FOREACH_TEST(prefix, condition, expcount) \
657    FOREACH_PRECONDITION(condition, expcount); \
658    r = quota_foreach(prefix, found_cb, &exphash, NULL); \
659    FOREACH_POSTCONDITION()
660
661static void notest_foreach(void)
662{
663    struct quota q;
664    struct quota q2;
665    struct txn *txn = NULL;
666    unsigned int n;
667    struct quota *expected;
668    hash_table exphash = HASH_TABLE_INITIALIZER;
669#define MAXN    4095
670    int r;
671
672    expected = (struct quota *)xzmalloc((MAXN+1) * sizeof(struct quota));
673    for (n = 0 ; n <= MAXN ; n++) {
674        expected[n].root = xstrdup(nth_quotaroot(n));
675        expected[n].useds[QUOTA_STORAGE] = nth_used(n);
676        expected[n].limits[QUOTA_STORAGE] = nth_limit(n);
677    }
678    construct_hash_table(&exphash, (MAXN+1)*4, 0);
679
680    /* add records to the db */
681    for (n = 0 ; n <= MAXN ; n++)
682    {
683        q = expected[n];
684        r = quota_write(&q, &txn);
685        CU_ASSERT_EQUAL(r, 0);
686        CU_ASSERT_PTR_NOT_NULL(txn);
687    }
688
689    quota_commit(&txn);
690    CU_ASSERT_PTR_NULL(txn);
691
692    /* check the records all made it */
693    for (n = 0 ; n <= MAXN ; n++)
694    {
695        memset(&q2, 0, sizeof(q2));
696        q2.root = nth_quotaroot(n);
697        r = quota_read(&q2, NULL, 0);
698        CU_ASSERT_EQUAL(r, 0);
699        CU_ASSERT_EQUAL(q2.useds[QUOTA_STORAGE],
700                        expected[n].useds[QUOTA_STORAGE]);
701        CU_ASSERT_EQUAL(q2.limits[QUOTA_STORAGE],
702                        expected[n].limits[QUOTA_STORAGE]);
703    }
704
705    /* prefix=NULL: iterate all the records */
706    FOREACH_TEST(NULL, 1, MAXN+1);
707
708    /* prefix="": iterate all the records */
709    FOREACH_TEST("", 1, MAXN+1);
710
711    /* prefix=a common prefix: iterate all the records */
712    FOREACH_TEST("user.", 1, MAXN+1);
713
714    /* prefix=an uncommon prefix: iterate some of the records */
715    FOREACH_TEST("user.epilogs", i / 1000 == 1, 1000);
716
717    /* delete records one by one, checking that foreach
718     * walks over the expected number at each point */
719    for (n = 0 ; n <= MAXN ; n++) {
720        r = quota_deleteroot(nth_quotaroot(n));
721        CU_ASSERT_EQUAL(r, 0);
722
723        if (n && n % 301 == 0) {
724            FOREACH_TEST("user.", i > n, MAXN-n);
725        }
726    }
727
728    free_hash_table(&exphash, NULL);
729    for (n = 0 ; n <= MAXN ; n++)
730        free((char *)expected[n].root);
731    free(expected);
732#undef MAXN
733}
734#endif
735
736/*
737 * TODO: should test for quota_foreach() iteration order,
738 *  with and without IMAPOPT_IMPROVED_MBOXLIST_SORT set.
739 *  There is code that depends on it, e.g. for quota -f.
740 */
741
742
743/* Note that quota_findroot() returns 0 (not found)
744 * or 1 (found) and never an error code. */
745#define TESTCASE(in, exp) \
746{ \
747    const char *expected = (exp); \
748    int r; \
749    char res[1024]; \
750    r = quota_findroot(res, sizeof(res), (in)); \
751    if (expected) { \
752        CU_ASSERT_EQUAL(r, 1); \
753        CU_ASSERT_STRING_EQUAL(res, expected); \
754    } \
755    else { \
756        CU_ASSERT_EQUAL(r, 0); \
757        /* contents of res[] not defined */ \
758    } \
759}
760
761static void test_findroot(void)
762{
763    struct quota q;
764    struct txn *txn = NULL;
765    int r;
766
767    memset(&q, 0, sizeof(q));
768    q.useds[QUOTA_STORAGE] = 123;
769    q.limits[QUOTA_STORAGE] = 456;
770
771    /*
772     * behaviour when the database is empty:
773     * there is no root to be found
774     */
775    TESTCASE("user.foo.bar.baz", NULL);
776    TESTCASE("user.foo.quux", NULL);
777    TESTCASE("user.foo", NULL);
778    TESTCASE("user.foonly", NULL);
779    TESTCASE("user.fo", NULL);
780    TESTCASE("user.smeg", NULL);
781    TESTCASE("user.smeg.fridge", NULL);
782    TESTCASE("user.farnarkle", NULL);
783
784    /* add some db entries, but not the "user." */
785    q.root = "user.foo";
786    r = quota_write(&q, &txn);
787    CU_ASSERT_EQUAL(r, 0);
788
789    q.root = "user.smeg";
790    r = quota_write(&q, &txn);
791    CU_ASSERT_EQUAL(r, 0);
792
793    CU_ASSERT_PTR_NOT_NULL(txn);
794    quota_commit(&txn);
795    CU_ASSERT_PTR_NULL(txn);
796
797    /*
798     * behaviour with these new entries
799     */
800    /* "user.foo" and its subfolders match the "user.foo" root */
801    TESTCASE("user.foo.bar.baz", "user.foo");
802    TESTCASE("user.foo.quux", "user.foo");
803    TESTCASE("user.foo", "user.foo");
804    /* "user.foonly" doesn't match "user.foo" despite the leading
805     * 8 characters being the same */
806    TESTCASE("user.foonly", NULL);
807    /* "user.fo" doesn't match "user.foo" despite the leading
808     * 7 characters being the same */
809    TESTCASE("user.fo", NULL);
810    /* "user.smeg" and its subfolders matches the "user.smeg" root */
811    TESTCASE("user.smeg", "user.smeg");
812    TESTCASE("user.smeg.fridge", "user.smeg");
813    /* no root matches at all for "user.farnarkle" */
814    TESTCASE("user.farnarkle", NULL);
815
816    /* add a catch-all "user" record */
817    q.root = "user";
818    r = quota_write(&q, &txn);
819    CU_ASSERT_EQUAL(r, 0);
820
821    CU_ASSERT_PTR_NOT_NULL(txn);
822    quota_commit(&txn);
823    CU_ASSERT_PTR_NULL(txn);
824
825    /*
826     * behaviour with these new entries
827     */
828    /* "user.foo" unchanged */
829    TESTCASE("user.foo.bar.baz", "user.foo");
830    TESTCASE("user.foo.quux", "user.foo");
831    TESTCASE("user.foo", "user.foo");
832    /* "user.foonly" matches the catch-all */
833    TESTCASE("user.foonly", "user");
834    /* "user.fo" ditto */
835    TESTCASE("user.fo", "user");
836    /* "user.smeg" unchanged */
837    TESTCASE("user.smeg", "user.smeg");
838    TESTCASE("user.smeg.fridge", "user.smeg");
839    /* "user.farnarkle" matches the catch-all */
840    TESTCASE("user.farnarkle", "user");
841}
842
843static void test_findroot_virtdomains(void)
844{
845    struct quota q;
846    struct txn *txn = NULL;
847    int r;
848
849    config_virtdomains = IMAP_ENUM_VIRTDOMAINS_ON;
850    /* this shouldn't matter, quota_findroot() doesn't use it */
851    config_defdomain = "smaak.nl";
852
853    memset(&q, 0, sizeof(q));
854    q.useds[QUOTA_STORAGE] = 123;
855    q.limits[QUOTA_STORAGE] = 456;
856
857    /*
858     * behaviour when the database is empty:
859     * there is no root to be found
860     */
861    TESTCASE("bloggs.com!user.foo.bar.baz", NULL);
862    TESTCASE("bloggs.com!user.foo.quux", NULL);
863    TESTCASE("bloggs.com!user.foo", NULL);
864    TESTCASE("fnarp.org!user.foo.bar.baz", NULL);
865    TESTCASE("fnarp.org!user.foo.quux", NULL);
866    TESTCASE("fnarp.org!user.foo", NULL);
867    TESTCASE("user.foo.bar.baz", NULL);
868    TESTCASE("user.foo.quux", NULL);
869    TESTCASE("user.foo", NULL);
870    TESTCASE("bloggs.com!user.smeg", NULL);
871    TESTCASE("bloggs.com!user.smeg.fridge", NULL);
872    TESTCASE("fnarp.org!user.smeg", NULL);
873    TESTCASE("fnarp.org!user.smeg.fridge", NULL);
874    TESTCASE("user.smeg", NULL);
875    TESTCASE("user.smeg.fridge", NULL);
876    TESTCASE("bloggs.com!user.farnarkle", NULL);
877    TESTCASE("fnarp.org!user.farnarkle", NULL);
878    TESTCASE("user.farnarkle", NULL);
879
880    /* add some db entries for bloggs.com, but not fnarp.org,
881     * not the default domain, and not the "user." */
882    q.root = "bloggs.com!user.foo";
883    r = quota_write(&q, &txn);
884    CU_ASSERT_EQUAL(r, 0);
885
886    q.root = "bloggs.com!user.smeg";
887    r = quota_write(&q, &txn);
888    CU_ASSERT_EQUAL(r, 0);
889
890    CU_ASSERT_PTR_NOT_NULL(txn);
891    quota_commit(&txn);
892    CU_ASSERT_PTR_NULL(txn);
893
894    /*
895     * behaviour with these new entries
896     */
897    /* foo@bloggs.com's home and its subfolders match
898     * the "bloggs.com!user.foo" root */
899    TESTCASE("bloggs.com!user.foo.bar.baz", "bloggs.com!user.foo");
900    TESTCASE("bloggs.com!user.foo.quux", "bloggs.com!user.foo");
901    TESTCASE("bloggs.com!user.foo", "bloggs.com!user.foo");
902    /* foo@fnarp.org's home and its subfolders don't match anything */
903    TESTCASE("fnarp.org!user.foo.bar.baz", NULL);
904    TESTCASE("fnarp.org!user.foo.quux", NULL);
905    TESTCASE("fnarp.org!user.foo", NULL);
906    /* foo's home and its subfolders don't match anything */
907    TESTCASE("user.foo.bar.baz", NULL);
908    TESTCASE("user.foo.quux", NULL);
909    TESTCASE("user.foo", NULL);
910    /* smeg@bloggs.com's home and its subfolders match
911     * the "bloggs.com!user.smeg" root */
912    TESTCASE("bloggs.com!user.smeg", "bloggs.com!user.smeg");
913    TESTCASE("bloggs.com!user.smeg.fridge", "bloggs.com!user.smeg");
914    /* smeg@fnarp.org's home and its subfolders don't match anything */
915    TESTCASE("fnarp.org!user.smeg", NULL);
916    TESTCASE("fnarp.org!user.smeg.fridge", NULL);
917    /* smeg's home and its subfolders don't match anything */
918    TESTCASE("user.smeg", NULL);
919    TESTCASE("user.smeg.fridge", NULL);
920    /* no root matches at all for "user.farnarkle" in any domain */
921    TESTCASE("bloggs.com!user.farnarkle", NULL);
922    TESTCASE("fnarp.org!user.farnarkle", NULL);
923    TESTCASE("user.farnarkle", NULL);
924
925    /* add a catch-all "bloggs.com" record */
926    q.root = "bloggs.com!";
927    r = quota_write(&q, &txn);
928    CU_ASSERT_EQUAL(r, 0);
929
930    CU_ASSERT_PTR_NOT_NULL(txn);
931    quota_commit(&txn);
932    CU_ASSERT_PTR_NULL(txn);
933
934    /*
935     * behaviour with these new entries
936     */
937    /* foo@bloggs.com unchanged */
938    TESTCASE("bloggs.com!user.foo.bar.baz", "bloggs.com!user.foo");
939    TESTCASE("bloggs.com!user.foo.quux", "bloggs.com!user.foo");
940    TESTCASE("bloggs.com!user.foo", "bloggs.com!user.foo");
941    /* foo@fnarp.org's unchanged */
942    TESTCASE("fnarp.org!user.foo.bar.baz", NULL);
943    TESTCASE("fnarp.org!user.foo.quux", NULL);
944    TESTCASE("fnarp.org!user.foo", NULL);
945    /* foo unchanged */
946    TESTCASE("user.foo.bar.baz", NULL);
947    TESTCASE("user.foo.quux", NULL);
948    TESTCASE("user.foo", NULL);
949    /* smeg@bloggs.com unchanged */
950    TESTCASE("bloggs.com!user.smeg", "bloggs.com!user.smeg");
951    TESTCASE("bloggs.com!user.smeg.fridge", "bloggs.com!user.smeg");
952    /* smeg@fnarp.org unchanged */
953    TESTCASE("fnarp.org!user.smeg", NULL);
954    TESTCASE("fnarp.org!user.smeg.fridge", NULL);
955    /* smeg unchanged */
956    TESTCASE("user.smeg", NULL);
957    TESTCASE("user.smeg.fridge", NULL);
958    /* farnarkle@bloggs.com matches the bloggs.com catch-all */
959    TESTCASE("bloggs.com!user.farnarkle", "bloggs.com!");
960    /* farnarkle at other domains unchanged */
961    TESTCASE("fnarp.org!user.farnarkle", NULL);
962    TESTCASE("user.farnarkle", NULL);
963
964    /* add a catch-all "fnarp.org!user" record */
965    q.root = "fnarp.org!user";
966    r = quota_write(&q, &txn);
967    CU_ASSERT_EQUAL(r, 0);
968
969    CU_ASSERT_PTR_NOT_NULL(txn);
970    quota_commit(&txn);
971    CU_ASSERT_PTR_NULL(txn);
972
973    /*
974     * behaviour with these new entries
975     */
976    /* foo@bloggs.com unchanged */
977    TESTCASE("bloggs.com!user.foo.bar.baz", "bloggs.com!user.foo");
978    TESTCASE("bloggs.com!user.foo.quux", "bloggs.com!user.foo");
979    TESTCASE("bloggs.com!user.foo", "bloggs.com!user.foo");
980    /* foo@fnarp.org's matches the fnarp.org catch-all */
981    TESTCASE("fnarp.org!user.foo.bar.baz", "fnarp.org!user");
982    TESTCASE("fnarp.org!user.foo.quux", "fnarp.org!user");
983    TESTCASE("fnarp.org!user.foo", "fnarp.org!user");
984    /* foo unchanged */
985    TESTCASE("user.foo.bar.baz", NULL);
986    TESTCASE("user.foo.quux", NULL);
987    TESTCASE("user.foo", NULL);
988    /* smeg@bloggs.com unchanged */
989    TESTCASE("bloggs.com!user.smeg", "bloggs.com!user.smeg");
990    TESTCASE("bloggs.com!user.smeg.fridge", "bloggs.com!user.smeg");
991    /* smeg@fnarp.org matches the fnarp.org catch-all */
992    TESTCASE("fnarp.org!user.smeg", "fnarp.org!user");
993    TESTCASE("fnarp.org!user.smeg.fridge", "fnarp.org!user");
994    /* smeg unchanged */
995    TESTCASE("user.smeg", NULL);
996    TESTCASE("user.smeg.fridge", NULL);
997    /* farnarkle@bloggs.com matches the bloggs.com catch-all */
998    TESTCASE("bloggs.com!user.farnarkle", "bloggs.com!");
999    /* farnarkle@fnarp.org matches the fnarp.org catch-all */
1000    TESTCASE("fnarp.org!user.farnarkle", "fnarp.org!user");
1001    /* farnarkle in the default domain unchanged */
1002    TESTCASE("user.farnarkle", NULL);
1003}
1004#undef TESTCASE
1005
1006static void config_read_string(const char *s)
1007{
1008    char *fname = xstrdup("/tmp/cyrus-cunit-configXXXXXX");
1009    int fd = mkstemp(fname);
1010    retry_write(fd, s, strlen(s));
1011    config_reset();
1012    config_read(fname, 0);
1013    unlink(fname);
1014    free(fname);
1015    close(fd);
1016}
1017
1018static int set_up(void)
1019{
1020    int r;
1021    const char * const *d;
1022    static const char * const dirs[] = {
1023        DBDIR,
1024        DBDIR"/db",
1025        NULL
1026    };
1027
1028    r = system("rm -rf " DBDIR);
1029    if (r)
1030        return r;
1031
1032    for (d = dirs ; *d ; d++) {
1033        r = mkdir(*d, 0777);
1034        if (r < 0) {
1035            int e = errno;
1036            perror(*d);
1037            return e;
1038        }
1039    }
1040
1041    libcyrus_config_setstring(CYRUSOPT_CONFIG_DIR, DBDIR);
1042    config_read_string(
1043        "configdirectory: "DBDIR"/conf\n"
1044    );
1045
1046    cyrusdb_init();
1047    config_quota_db = "skiplist";
1048
1049    quotadb_init(0);
1050    quotadb_open(NULL);
1051
1052    return 0;
1053}
1054
1055static int tear_down(void)
1056{
1057    int r;
1058
1059    quotadb_close();
1060    quotadb_done();
1061    cyrusdb_done();
1062    config_quota_db = NULL;
1063    config_reset();
1064
1065    r = system("rm -rf " DBDIR);
1066    if (r) r = -1;
1067
1068    return r;
1069}
1070/* vim: set ft=c: */
1071