]> git.ipfire.org Git - thirdparty/postfix.git/commitdiff
postfix-3.8-20220816-nonprod 20220724-nonprod
authorWietse Venema <wietse@porcupine.org>
Tue, 16 Aug 2022 05:00:00 +0000 (00:00 -0500)
committerViktor Dukhovni <postfix-users@dukhovni.org>
Sun, 21 Aug 2022 20:54:48 +0000 (16:54 -0400)
68 files changed:
postfix/.indent.pro
postfix/HISTORY
postfix/README_FILES/AAAREADME
postfix/README_FILES/PTEST_README [new file with mode: 0644]
postfix/README_FILES/SASL_README
postfix/README_FILES/SMTPD_POLICY_README
postfix/TODO
postfix/WISHLIST
postfix/conf/aliases
postfix/conf/postfix-files
postfix/html/PTEST_README.html [new file with mode: 0644]
postfix/html/SASL_README.html
postfix/html/SMTPD_POLICY_README.html
postfix/html/aliases.5.html
postfix/html/index.html
postfix/html/postconf.5.html
postfix/man/man5/aliases.5
postfix/man/man5/postconf.5
postfix/mantools/postlink
postfix/proto/Makefile.in
postfix/proto/PTEST_README.html [new file with mode: 0644]
postfix/proto/SASL_README.html
postfix/proto/SMTPD_POLICY_README.html
postfix/proto/aliases
postfix/proto/postconf.proto
postfix/proto/stop.double-proto-html
postfix/proto/stop.spell-cc
postfix/proto/stop.spell-proto-html
postfix/src/global/Makefile.in
postfix/src/global/haproxy_srvr.c
postfix/src/global/haproxy_srvr.h
postfix/src/global/haproxy_srvr_test.c [new file with mode: 0644]
postfix/src/global/mail_proto.h
postfix/src/global/mail_version.h
postfix/src/global/map_search_test.c
postfix/src/postscreen/Makefile.in
postfix/src/postscreen/postscreen.h
postfix/src/postscreen/postscreen_dnsbl.c
postfix/src/postscreen/postscreen_dnsbl_test.c [new file with mode: 0644]
postfix/src/ptest/ptest.h
postfix/src/ptest/ptest_main.h
postfix/src/ptest/ptest_run.c
postfix/src/smtpd/Makefile.in
postfix/src/smtpd/smtpd_check.c
postfix/src/testing/Makefile.in
postfix/src/testing/make_attr.c [new file with mode: 0644]
postfix/src/testing/make_attr.h [new file with mode: 0644]
postfix/src/testing/match_addr_test.c
postfix/src/testing/match_attr.c [new file with mode: 0644]
postfix/src/testing/match_attr.h [new file with mode: 0644]
postfix/src/testing/match_attr_test.c [new file with mode: 0644]
postfix/src/testing/mock_server.c [new file with mode: 0644]
postfix/src/testing/mock_server.h [new file with mode: 0644]
postfix/src/testing/mock_server_test.c [new file with mode: 0644]
postfix/src/util/argv_test.c
postfix/src/util/dict_pipe.c
postfix/src/util/dict_pipe_test.c
postfix/src/util/dict_stream_test.c
postfix/src/util/dict_union_test.c
postfix/src/util/find_inet_service_test.c
postfix/src/util/hash_fnv_test.c
postfix/src/util/known_tcp_ports_test.c
postfix/src/util/msg_output_test.c
postfix/src/util/myaddrinfo_test.c
postfix/src/util/mymalloc_test.c
postfix/src/util/mystrtok_test.c
postfix/src/util/unescape.c
postfix/src/util/unescape_test.c

index 7a382f767e0b8cd53b4ab9e48ef1eed287c81743..0c4dfbefd94c145a06782223bb767d1a6b5a8884 100644 (file)
@@ -1,5 +1,4 @@
 -TABOUNCE_STATE
--Taddrinfo
 -TADDR_MATCH_LIST
 -TADDR_PATTERN
 -TALIAS_TOKEN
@@ -21,7 +20,6 @@
 -TBH_TABLE
 -TBINATTR
 -TBINATTR_INFO
--Tbind_props
 -TBINHASH
 -TBINHASH_INFO
 -TBIO
 -TBYTE_MASK
 -TCFG_PARSER
 -TCIDR_MATCH
--Tcipher_probe_t
 -TCLEANUP_REGION
--TCLEANUP_STAT_DETAIL
 -TCLEANUP_STATE
+-TCLEANUP_STAT_DETAIL
 -TCLIENT_LIST
 -TCLNT_STREAM
 -TCONFIG_BOOL_FN_TABLE
 -TCRYPTO_EX_DATA
 -TCTABLE
 -TCTABLE_ENTRY
--Td2i_X509_t
--Tdane_digest
--Tdane_mtype
 -TDB_COMMON_CTX
--TDELIVER_ATTR
 -TDELIVERED_HDR_INFO
+-TDELIVER_ATTR
 -TDELIVER_REQUEST
 -TDELTA_TIME
 -TDICT
 -TEVP_PKEY
 -TEXPAND_ATTR
 -TFILE
--Tfilter_ctx
 -TFORWARD_INFO
--Tgeneral_name_stack_t
 -THBC_ACTION_CALL_BACKS
 -THBC_CALL_BACKS
 -THBC_CHECKS
 -THOST
 -THTABLE
 -THTABLE_INFO
--Tiana_digest
 -TINET_ADDR_LIST
 -TINET_PROTO_INFO
 -TINSTANCE
 -TINST_SELECTION
 -TINT32_TYPE
--TINT_TABLE
 -TINTV
+-TINT_TABLE
 -TJMP_BUF_WRAPPER
 -TLDAP
--TLDAP_CONN
 -TLDAPMessage
 -TLDAPURLDesc
+-TLDAP_CONN
 -TLIB_DP
 -TLIB_FN
 -TLMTP_ATTR
 -TMAC_EXP_OP_INFO
 -TMAC_HEAD
 -TMAC_PARSE
--TMAI_HOSTADDR_STR
--TMAI_HOSTNAME_STR
 -TMAIL_ADDR_FORMATTER
 -TMAIL_ADDR_MAP_TEST
 -TMAIL_PRINT
 -TMAIL_SCAN
 -TMAIL_STREAM
 -TMAIL_VERSION
+-TMAI_HOSTADDR_STR
+-TMAI_HOSTNAME_STR
 -TMAI_SERVNAME_STR
 -TMAI_SERVPORT_STR
 -TMAPS
 -TMDB_val
 -TMILTER
 -TMILTER8
+-TMILTERS
 -TMILTER_MACROS
 -TMILTER_MSG_CONTEXT
--TMILTERS
 -TMIME_ENCODING
 -TMIME_INFO
 -TMIME_STACK
 -TMOCK_APPL_SIG
 -TMOCK_APPL_STATUS
 -TMOCK_EXPECT
+-TMOCK_UNIX_SERVER
 -TMSG_OUTPUT_INFO
 -TMSG_STATS
 -TMULTI_SERVER
 -TNAME_MASK
 -TNBBIO
 -TNVTABLE_INFO
--Toff_t
 -TOPTIONS
 -TPCF_DBMS_INFO
 -TPCF_EVAL_CTX
 -TPCF_SERVICE_PATTERN
 -TPCF_STRING_NV
 -TPEER_NAME
--Tpem_load_state_t
 -TPGSQL_NAME
 -TPICKUP_INFO
 -TPIPE_ATTR
 -TPIPE_STATE
 -TPLMYSQL
 -TPLPGSQL
+-TPOSTMAP_KEY_STATE
 -TPOST_MAIL_FCLOSE_STATE
 -TPOST_MAIL_STATE
--TPOSTMAP_KEY_STATE
 -TPRIVATE_STR_TABLE
 -TPSC_CALL_BACK_ENTRY
 -TPSC_CLIENT_INFO
 -TRECIPIENT
 -TRECIPIENT_LIST
 -TREC_TYPE_NAME
--Tregex_t
--Tregmatch_t
--TRES_CONTEXT
 -TRESOLVE_REPLY
 -TRESPONSE
 -TREST_TABLE
+-TRES_CONTEXT
 -TRWR_CONTEXT
--Tsasl_conn_t
--Tsasl_secret_t
 -TSCACHE
 -TSCACHE_CLNT
 -TSCACHE_MULTI
 -TSENDER_LOGIN_MATCH
 -TSERVER_AC
 -TSESSION
--Tsfsistat
 -TSHARED_PATH
--Tsigset_t
 -TSINGLE_SERVER
 -TSINK_COMMAND
 -TSINK_STATE
--Tsize_t
 -TSLMDB
 -TSMFICTX
--TSM_STATE
--TSMTP_ADDR
--TSMTP_CLI_ATTR
--TSMTP_CMD
 -TSMTPD_CMD
 -TSMTPD_DEFER
 -TSMTPD_ENDPT_LOOKUP_INFO
 -TSMTPD_STATE
 -TSMTPD_TOKEN
 -TSMTPD_XFORWARD_ATTR
+-TSMTP_ADDR
+-TSMTP_CLI_ATTR
+-TSMTP_CMD
 -TSMTP_ITERATOR
 -TSMTP_RESP
 -TSMTP_SASL_AUTH_CACHE
 -TSMTP_TLS_POLICY
 -TSMTP_TLS_SESS
 -TSMTP_TLS_SITE_POLICY
--Tsockaddr
+-TSM_STATE
 -TSOCKADDR_SIZE
 -TSPAWN_ATTR
--Tssize_t
 -TSSL
--Tssl_cipher_stack_t
--Tssl_comp_stack_t
 -TSSL_CTX
 -TSSL_SESSION
 -TSTATE
 -TSTRING_TABLE
 -TSYS_EXITS_DETAIL
 -TTEST_JMP_BUF
--Ttime_t
--Ttlsa_filter
+-TTLSMGR_SCACHE
+-TTLSP_STATE
 -TTLS_APPL_STATE
 -TTLS_CERTS
 -TTLS_CLIENT_INIT_PROPS
 -TTLS_CLIENT_PARAMS
 -TTLS_CLIENT_START_PROPS
--TTLScontext_t
 -TTLS_DANE
--TTLSMGR_SCACHE
 -TTLS_PKEYS
 -TTLS_PRNG_SEED_INFO
 -TTLS_PRNG_SRC
--TTLSP_STATE
 -TTLS_ROLE
 -TTLS_SCACHE
 -TTLS_SCACHE_ENTRY
 -TTLS_TLSA
 -TTLS_USAGE
 -TTLS_VINFO
+-TTLScontext_t
 -TTOK822
 -TTRANSPORT_INFO
 -TTRIGGER_SERVER
--Tuint16_t
--Tuint32_t
--Tuint8_t
 -TUSER_ATTR
 -TVBUF
 -TVSTREAM
 -TWATCHDOG
 -TWATCH_FD
 -TX509
+-TX509V3_CTX
 -TX509_EXTENSION
 -TX509_NAME
--Tx509_stack_t
 -TX509_STORE_CTX
--TX509V3_CTX
 -TXSASL_CLIENT
 -TXSASL_CLIENT_CREATE_ARGS
 -TXSASL_CLIENT_IMPL
 -TXSASL_SERVER_CREATE_ARGS
 -TXSASL_SERVER_IMPL
 -TXSASL_SERVER_IMPL_INFO
+-Taddrinfo
+-Tbind_props
+-Tcipher_probe_t
+-Td2i_X509_t
+-Tdane_digest
+-Tdane_mtype
+-Tfilter_ctx
+-Tgeneral_name_stack_t
+-Tiana_digest
+-Toff_t
+-Tpem_load_state_t
+-Tregex_t
+-Tregmatch_t
+-Tsasl_conn_t
+-Tsasl_secret_t
+-Tsfsistat
+-Tsigset_t
+-Tsize_t
+-Tsockaddr
+-Tssize_t
+-Tssl_cipher_stack_t
+-Tssl_comp_stack_t
+-Ttime_t
+-Ttlsa_filter
+-Tuint16_t
+-Tuint32_t
+-Tuint8_t
+-Tx509_stack_t
index aca90aee1ede7a9a6694c3598df9391ac688c172..c99db9294f350184457b0d118b67100bd3f1a8f7 100644 (file)
@@ -26590,10 +26590,42 @@ Apologies for any names omitted.
        Report by Spil Oss, fix by Viktor Dukhovni. File:
        tls/tls_server.c.
 
+20220802
+
+       Documentation: in the aliases(5) manpage, more specific
+       pointers to the local(8) manpage sections for delivery to
+       file, command execution, and delivery rights. File:
+       proto/aliases.
+
+20220805
+
+       Feature: "mail_version" attribute in the SMTPD policy
+       protocol, with the value of the "mail_version" configuration
+       parameter. This differs from the "compatibility_level"
+       attribute, because "mail_version" indicates the presence
+       of new features, while "compatibility_level" concerns changes
+       in default settings. Files: global/mail_proto.h,
+       proto/SMTPD_POLICY_README.html, smtpd/smtpd_check.c.
+
+20220808
+
+       Documentation: some Debian releases hard-code the search
+       path for Cyrus SASL application configuration files,
+       overriding the cyrus_sasl_config_path setting. Viktor
+       Dukhovni. File: proto/SASL_README.html.
+
+20220815
+
+       Updated the postscreen_dnsbl_sites documentation, based
+       on questions on the postfix-users mailing list.  File:
+       proto/postconf.proto.
+
        Feature: 'ptest' infrastructure for unit tests, and 'pmock'
        infrastructure to make tests independent of host configuration,
        network configuration, or DNS. ptest looks like Go test,
-       while pmock implements a few ideas from Google gmock.
+       while pmock implements a few ideas from Google gmock. The
+       PTEST_README file has some information about how Postfix
+       unit tests work.
 
        This changes the Postfix file footprint as follows:
 
index 9afa3b7d26e75d464862ef21b3d38799a8b3b8f0..c82d99046ecca6c7fb48981d625d9e4e3026351c 100644 (file)
@@ -84,3 +84,7 @@ O\bOt\bth\bhe\ber\br t\bto\bop\bpi\bic\bcs\bs
   * XCLIENT_README: XCLIENT Command
   * XFORWARD_README: XFORWARD Command
 
+F\bFo\bor\br m\bma\bai\bin\bnt\bta\bai\bin\bne\ber\brs\bs a\ban\bnd\bd c\bco\bon\bnt\btr\bri\bib\bbu\but\bto\bor\brs\bs
+
+  * PTEST_README: Writing Postfix unit tests
+
diff --git a/postfix/README_FILES/PTEST_README b/postfix/README_FILES/PTEST_README
new file mode 100644 (file)
index 0000000..78d5d48
--- /dev/null
@@ -0,0 +1,444 @@
+W\bWr\bri\bit\bti\bin\bng\bg P\bPo\bos\bst\btf\bfi\bix\bx u\bun\bni\bit\bt t\bte\bes\bst\bts\bs
+
+-------------------------------------------------------------------------------
+
+O\bOv\bve\ber\brv\bvi\bie\bew\bw
+
+This document covers, Ptest, a simple unit test framework that was introduced
+with Postfix version 3.8. It is modeled after Go tests, with primitives such as
+ptest_error() and ptest_fatal() that report test failures, and PTEST_RUN() that
+supports subtests.
+
+Ptest is light-weight compared to more powerful framweworks such as Gtest, but
+it avoids the need for adding a large Postfix dependency (a dependency that
+would not affect Postfix distributors, but developers only).
+
+  * Simple example
+
+  * Testing one function with TEST_CASE data
+
+  * Testing functions with subtests
+
+  * Suggestions for writing tests
+
+  * Ptest API reference
+
+S\bSi\bim\bmp\bpl\ble\be e\bex\bxa\bam\bmp\bpl\ble\be
+
+Simple tests exercise one function under test, one scenario at a time. Each
+scenario calls the function under test with good or bad inputs, and verifies
+that the function behaves as expected. The code in Postfix mymalloc_test.c file
+is a good example.
+
+After some #include statements, the file goes like this:
+
+     27 typedef struct PTEST_CASE {
+     28     const char *testname;               /* Human-readable description
+    */
+     29     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+     30 } PTEST_CASE;
+     31
+     32 /* Test functions. */
+     33
+     34 static void test_mymalloc_normal(PTEST_CTX *t, const PTEST_CASE *tp)
+     35 {
+     36     void   *ptr;
+     37
+     38     ptr = mymalloc(100);
+     39     myfree(ptr);
+     40 }
+     41
+     42 static void test_mymalloc_panic_too_small(PTEST_CTX *t, const
+    PTEST_CASE *tp)
+     43 {
+     44     expect_ptest_log_event(t, "panic: mymalloc: requested length 0");
+     45     (void) mymalloc(0);
+     46     ptest_fatal(t, "mymalloc(0) returned");
+     47 }
+    ...     // Test functions for myrealloc(), mystrdup(), mymemdup().
+    260
+    261 static const PTEST_CASE ptestcases[] = {
+    262     {"mymalloc + myfree normal case", test_mymalloc_normal,
+    263     },
+    264     {"mymalloc panic for too small request",
+    test_mymalloc_panic_too_small,
+    265     },
+    ...     // Test cases for myrealloc(), mystrdup(), mymemdup().
+    306 };
+    307
+    308 #include <ptest_main.h>
+
+To run the test:
+
+    $ make test_mymalloc
+    ... compiler output...
+    LD_LIBRARY_PATH=/path/to/postfix-source/lib ./mymalloc_test
+    RUN  mymalloc + myfree normal case
+    PASS mymalloc + myfree normal case
+    RUN  mymalloc panic for too small request
+    PASS mymalloc panic for too small request
+    ... results for myrealloc(), mystrdup(), mymemdup()...
+    mymalloc_test: PASS: 22, SKIP: 0, FAIL: 0
+
+This simple example already shows several key features of the ptest framework.
+
+  * Each test is implemented as a separate function (test_mymalloc_normal(),
+    test_mymalloc_panic_too_small(), and so on).
+
+  * The first test verifies 'normal' behavior: it verifies that mymalloc() will
+    allocate a small amount of memory, and that myfree() will accept the result
+    from mymalloc(). When the test is run under a memory checker such as
+    Valgrind, the memory checker will report no memory leak or other error.
+
+  * The second test is more interesting.
+
+      o The test verifies that mymalloc() will call msg_panic() when the
+        requested amount of memory is too small. But in this test the msg_panic
+        () call will not terminate the process like it normally would. The
+        Ptest framework changes the control flow of msg_panic() and msg_fatal()
+        such that these functions will terminate their test, instead of their
+        process.
+
+      o The expect_ptest_log_event() call sets up an expectation that msg_panic
+        () will produce a specific error message; the test would fail if the
+        expectation remains unsatisfied.
+
+      o The ptest_fatal() call at the end of the second test is not needed;
+        this call can only be reached if mymalloc() does not call msg_panic().
+        But then the expected panic message will not be logged, and the test
+        will fail anyway.
+
+  * The ptestcases[] table near the end of the example contains for each test
+    the name and a pointer to function. As we show in a later example, the
+    ptestcases[] table can also contain test inputs and expectations.
+
+  * The "#include <ptest_main.h>" at the end pulls in the code that iterates
+    over the ptestcases[] table and logs progress.
+
+  * The test run output shows that the msg_panic() output in the second test is
+    silenced; only output from unexpected msg_panic() or other unexpected msg
+    (3) calls would show up in test run output.
+
+T\bTe\bes\bst\bti\bin\bng\bg o\bon\bne\be f\bfu\bun\bnc\bct\bti\bio\bon\bn w\bwi\bit\bth\bh T\bTE\bES\bST\bT_\b_C\bCA\bAS\bSE\bE d\bda\bat\bta\ba
+
+Often, we want to test a module that contains only one function. In that case
+we can store all the test inputs and expected results in the PTEST_CASE
+structure.
+
+The examples below are taken from the dict_union_test.c file which test the
+unionmap implementation in the file. dict_union.c.
+
+Background: a unionmap creates a union of tables. For example, the lookup table
+"unionmap:{inline:{foo=one},inline:{foo=two}}" will return ("one, two",
+DICT_STAT_SUCCESS) when queried with foo, and will return (NOTFOUND,
+DICT_STAT_SUCCESS) otherwise.
+
+First, we present the TEST_CASE structure with additional fields for inputs and
+expected results.
+
+     29 #define MAX_PROBE       5
+     30
+     31 struct probe {
+     32     const char *query;
+     33     const char *want_value;
+     34     int     want_error;
+     35 };
+     36
+     37 typedef struct PTEST_CASE {
+     38     const char *testname;
+     39     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+     40     const char *type_name;
+     41     const struct probe probes[MAX_PROBE];
+     42 } PTEST_CASE;
+
+In the PTEST_CASE structure above:
+
+  * The testname and action fields are standard. We have seen these already in
+    the simple example above.
+
+  * The type_name field will contain the name of the table, for example
+    unionmap:{static:one,inline:{foo=two}}.
+
+  * The probes field contains a list of (query, expected result value, expected
+    error code) that will be used to query the unionmap and to verify the
+    result value and error code.
+
+Next we show the test data. Every test calls the same test_dict_union()
+function with a different unionmap configuration and with a list of queries
+with expected results. The implementation of that function follows after the
+test data.
+
+     78 static const PTEST_CASE ptestcases[] = {
+     79     {
+     80          /* testname */ "successful lookup: static map + inline map",
+     81          /* action */ test_dict_union,
+     82          /* type_name */ "unionmap:{static:one,inline:{foo=two}}",
+     83          /* probes */ {
+     84             {"foo", "one,two", DICT_STAT_SUCCESS},
+     85             {"bar", "one", DICT_STAT_SUCCESS},
+     86         },
+     87     }, {
+     88          /* testname */ "error propagation: static map + fail map",
+     89          /* action */ test_dict_union,
+     90          /* type_name */ "unionmap:{static:one,fail:fail}",
+     91          /* probes */ {
+     92             {"foo", 0, DICT_STAT_ERROR},
+     93         },
+    ...
+    102 };
+    103
+    104 #include <ptest_main.h>
+
+Finally, here is the test_dict_union() function that tests the unionmap
+implementation with a given configuration and test queries.
+
+     44 #define STR_OR_NULL(s)  ((s) ? (s) : "null")
+     45
+     46 static void test_dict_union(PTEST_CTX *t, const struct PTEST_CASE *tp)
+     47 {
+     48     DICT   *dict;
+     49     const struct probe *pp;
+     50     const char *got_value;
+     51     int     got_error;
+     52
+     53     if ((dict = dict_open(tp->type_name, O_RDONLY, 0)) == 0)
+     54         ptest_fatal(t, "dict_open(\"%s\", O_RDONLY, 0) failed: %m",
+     55                     tp->type_name);
+     56     for (pp = tp->probes; pp < tp->probes + MAX_PROBE && pp->query !=
+    0; pp++) {
+     57         got_value = dict_get(dict, pp->query);
+     58         got_error = dict->error;
+     59         if (got_value == 0 && pp->want_value == 0)
+     60             continue;
+     61         if (got_value == 0 || pp->want_value == 0) {
+     62             ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want
+    '%s'",
+     63                         pp->query, STR_OR_NULL(got_value),
+     64                         STR_OR_NULL(pp->want_value));
+     65             break;
+     66         }
+     67         if (strcmp(got_value, pp->want_value) != 0) {
+     68             ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want
+    '%s'",
+     69                         pp->query, got_value, pp->want_value);
+     70         }
+     71         if (got_error != pp->want_error)
+     72             ptest_error(t, "dict_get(dict,\"%s\") error: got %d, want
+    %d",
+     73                         pp->query, got_error, pp->want_error);
+     74     }
+     75     dict_free(dict);
+     76 }
+
+A test run looks like this:
+
+    $ make test_dict_union
+    ...compiler output...
+    LD_LIBRARY_PATH=/path/to/postfix-source/lib ./dict_union_test
+    RUN  successful lookup: static map + inline map
+    PASS successful lookup: static map + inline map
+    RUN  error propagation: static map + fail map
+    PASS error propagation: static map + fail map
+    ...
+    dict_union_test: PASS: 3, SKIP: 0, FAIL: 0
+
+T\bTe\bes\bst\bti\bin\bng\bg f\bfu\bun\bnc\bct\bti\bio\bon\bns\bs w\bwi\bit\bth\bh s\bsu\bub\bbt\bte\bes\bst\bts\bs
+
+Sometimes it is not convenient to store test data in a PTEST_CASE structure.
+This can happen when converting an existing test into Ptest, or when the module
+under test contains multiple functions that need different kinds of test data.
+The solution is to create a _test.c file with the structure shown below. The
+example is based on code in map_search_test.c that was converted from an
+existing test into Ptest.
+
+  * One PTEST_CASE structure definition without test data.
+
+     50 typedef struct PTEST_CASE {
+     51     const char *testname;
+     52     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+     53 } PTEST_CASE;
+
+  * One test function for each module function that needs to be tested, and one
+    table with test cases for that module function. In this case there is only
+    one module function (map_search()) that needs to be tested, so there is
+    only one test function (test_map_search()).
+
+     67 #define MAX_WANT_LOG    5
+     68
+     69 static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE
+    *unused)
+     70 {
+     71     /* Test cases with inputs and expected outputs. */
+     72     struct test {
+     73         const char *map_spec;
+     74         int     want_return;            /* 0=fail, 1=success */
+     75         const char *want_log[MAX_WANT_LOG];
+     76         const char *want_map_type_name; /* 0 or match */
+     77         const char *exp_search_order;   /* 0 or match */
+     78     };
+     79     static struct test test_cases[] = {
+     80         { /* 0 */ "type", 0, {
+     81                 "malformed map specification: 'type'",
+     82                 "expected maptype:mapname instead of 'type'",
+     83         }, 0},
+    ...        // ...other test cases...
+    111     };
+
+  * In a test function, iterate over its table with test cases, using PTEST_RUN
+    () to run each test case in its own subtest.
+
+    129     for (tp = test_cases; tp->map_spec; tp++) {
+    130         vstring_sprintf(test_label, "test %d", (int) (tp -
+    test_cases));
+    131         PTEST_RUN(t, STR(test_label), {
+    132             for (cpp = tp->want_log; cpp < tp->want_log + MAX_WANT_LOG
+    && *cpp; cpp++)
+    133                 expect_ptest_log_event(t, *cpp);
+    134             map_search_from_create = map_search_create(tp->map_spec);
+    ...            // ...verify that the result is as expected...
+    ...            // ...use ptest_return() or ptest_fatal() to exit from a
+    test...
+    173         });
+    174     }
+    ...
+    178 }
+
+  * Create a ptestcases[] table to call each test function once, and include
+    the Ptest main program.
+
+    183 static const PTEST_CASE ptestcases[] = {
+    184     "test_map_search", test_map_search,
+    185 };
+    186
+    187 #include <ptest_main.h>
+
+See the file map_search_test.c for a complete example.
+
+This is what a test run looks like:
+
+    $ make test_map_search
+    ...compiler output...
+    LD_LIBRARY_PATH=/path/to/postfix-source/lib  ./map_search_test
+    RUN  test_map_search
+    RUN  test_map_search/test 0
+    PASS test_map_search/test 0
+    ....
+    PASS test_map_search
+    map_search_test: PASS: 13, SKIP: 0, FAIL: 0
+
+This shows that the subtest name is appended to the parent test name, formatted
+as parent-name/child-name.
+
+S\bSu\bug\bgg\bge\bes\bst\bti\bio\bon\bns\bs f\bfo\bor\br w\bwr\bri\bit\bti\bin\bng\bg t\bte\bes\bst\bts\bs
+
+Ptest is loosely inspired on Go test, especially its top-level test functions
+and its methods T.run(), T.error() and T.fatal().
+
+Suggestions for test style may look familiar to Go programmers:
+
+  * Use variables named got_xxx and want_xxx, and when a test result is
+    unexpected, log the discrepancy as "got <what you got>, want <what you
+    want>".
+
+  * Report discrepancies with ptest_error() if possible; use ptest_fatal() only
+    when continuing the test would produce nonsensical results.
+
+  * Where it makes sense use a table with testcases and use PTEST_RUN() to run
+    each testcase in its own subtest.
+
+Other suggestions:
+
+  * Consider running tests under a memory checker such as Valgrind. Use
+    ptest_defer() to avoid memory leaks when a test may terminate early.
+
+  * Always test non-error and error cases, to cover all code paths in the
+    function under test.
+
+P\bPt\bte\bes\bst\bt A\bAP\bPI\bI r\bre\bef\bfe\ber\bre\ben\bnc\bce\be
+
+  * Managing test errors
+
+  * Managing log events
+
+  * Managing test execution
+
+M\bMa\ban\bna\bag\bgi\bin\bng\bg t\bte\bes\bst\bt e\ber\brr\bro\bor\brs\bs
+
+As one might expect, Ptest has support to flag unexpected test results as
+errors.
+
+v\bvo\boi\bid\bd p\bpt\bte\bes\bst\bt_\b_e\ber\brr\bro\bor\br(\b(P\bPT\bTE\bES\bST\bT_\b_C\bCT\bTX\bX *\b*t\bt,\b, c\bco\bon\bns\bst\bt c\bch\bha\bar\br *\b*f\bfo\bor\brm\bma\bat\bt,\b, .\b..\b..\b.)\b)
+    Called from inside a test to report an unexpected test result, and to flag
+    the test as failed without terminating the test. This call can be ignored
+    with expect_ptest_error().
+
+v\bvo\boi\bid\bd p\bpt\bte\bes\bst\bt_\b_f\bfa\bat\bta\bal\bl(\b(P\bPT\bTE\bES\bST\bT_\b_C\bCT\bTX\bX *\b*t\bt,\b, c\bco\bon\bns\bst\bt c\bch\bha\bar\br *\b*f\bfo\bor\brm\bma\bat\bt,\b, .\b..\b..\b.)\b)
+    Called from inside a test to report an unexpected test result, to flag the
+    test as failed, and to terminate the test. This call cannot be ignored with
+    expect_ptest_error().
+For convenience, Ptest has can also report non-error information.
+
+v\bvo\boi\bid\bd p\bpt\bte\bes\bst\bt_\b_i\bin\bnf\bfo\bo(\b(P\bPT\bTE\bES\bST\bT_\b_C\bCT\bTX\bX *\b*t\bt,\b, c\bco\bon\bns\bst\bt c\bch\bha\bar\br *\b*f\bfo\bor\brm\bma\bat\bt,\b, .\b..\b..\b.)\b)
+    Called from inside a test to report a non-error condition without
+    terminating the test. This call cannot be ignored with expect_ptest_error
+    ().
+Finally, Ptest has support to test ptest_error() itself, to verify that an
+intentional error is reported as expected.
+
+v\bvo\boi\bid\bd e\bex\bxp\bpe\bec\bct\bt_\b_p\bpt\bte\bes\bst\bt_\b_e\ber\brr\bro\bor\br(\b(P\bPT\bTE\bES\bST\bT_\b_C\bCT\bTX\bX *\b*t\bt,\b, c\bco\bon\bns\bst\bt c\bch\bha\bar\br *\b*t\bte\bex\bxt\bt)\b)
+    Called from inside a test to expect exactly one ptest_error() call with the
+    specified text, and to ignore that ptest_error() call (i.e. don't flag the
+    test as failed). To ignore multiple calls, call expect_ptest_error()
+    multiple times. A test is flagged as failed when an expected error is not
+    reported (and of course when an error is reported that is not expected with
+    expect_ptest_error()).
+
+M\bMa\ban\bna\bag\bgi\bin\bng\bg l\blo\bog\bg e\bev\bve\ben\bnt\bts\bs
+
+Ptest integrates with Postfix msg(3) logging.
+
+  * Ptest changes the control flow of msg_fatal() and msg_panic(). When these
+    functions are called during a test, Ptest flags a test as failed and
+    terminates the test instead of the process.
+
+  * Ptest silences the output from msg_info() and other msg(3) calls, and
+    installs a log event listener tp monitor Postfix logging.
+
+Ptest provides the following API to manage log events:
+
+v\bvo\boi\bid\bd e\bex\bxp\bpe\bec\bct\bt_\b_p\bpt\bte\bes\bst\bt_\b_l\blo\bog\bg_\b_e\bev\bve\ben\bnt\bt(\b(P\bPT\bTE\bES\bST\bT_\b_C\bCT\bTX\bX *\b*t\bt,\b, c\bco\bon\bns\bst\bt c\bch\bha\bar\br *\b*t\bte\bex\bxt\bt)\b)
+    Called from inside a test to expect exactly one msg(3) call with the
+    specified text. To expect multiple events, call expect_ptest_log_event()
+    multiple times. A test is flagged as failed when expected text is not
+    logged, or when text is logged that is not expected with
+    expect_ptest_log_event().
+
+M\bMa\ban\bna\bag\bgi\bin\bng\bg t\bte\bes\bst\bt e\bex\bxe\bec\bcu\but\bti\bio\bon\bn
+
+Ptest has a number of primitives that control test execution.
+
+v\bvo\boi\bid\bd P\bPT\bTE\bES\bST\bT_\b_R\bRU\bUN\bN(\b(P\bPT\bTE\bES\bST\bT_\b_C\bCT\bTX\bX *\b*t\bt,\b, c\bco\bon\bns\bst\bt c\bch\bha\bar\br *\b*t\bte\bes\bst\bt_\b_n\bna\bam\bme\be,\b, {\b{ c\bco\bod\bde\be i\bin\bn b\bbr\bra\bac\bce\bes\bs }\b})\b)
+    Called from inside a test to run the { code in braces } in it own subtest
+    environment. In the test progress report, the subtest name is appended to
+    the parent test name, formatted as parent-name/child-name.
+
+    NOTE: because PTEST_RUN() is a macro, the { code in braces } must not
+    contain a return statement; use ptest_return() instead. It is OK for { code
+    in braces } to call a function that uses return.
+
+N\bNO\bOR\bRE\bET\bTU\bUR\bRN\bN p\bpt\bte\bes\bst\bt_\b_s\bsk\bki\bip\bp(\b(P\bPT\bTE\bES\bST\bT_\b_C\bCT\bTX\bX *\b*t\bt)\b)
+    Called from inside a test to flag a test as skipped, and to terminate the
+    test without terminating the process. Use this to disable tests that are
+    not applicable for a specific system type or build configuration.
+
+N\bNO\bOR\bRE\bET\bTU\bUR\bRN\bN p\bpt\bte\bes\bst\bt_\b_r\bre\bet\btu\bur\brn\bn(\b(P\bPT\bTE\bES\bST\bT_\b_C\bCT\bTX\bX *\b*t\bt)\b)
+    Called from inside a test to terminate the test without terminating the
+    process.
+
+v\bvo\boi\bid\bd p\bpt\bte\bes\bst\bt_\b_d\bde\bef\bfe\ber\br(\b(P\bPT\bTE\bES\bST\bT_\b_C\bCT\bTX\bX *\b*t\bt,\b, v\bvo\boi\bid\bd (\b(*\b*d\bde\bef\bfe\ber\br_\b_f\bfn\bn)\b)(\b(v\bvo\boi\bid\bd *\b*)\b),\b, v\bvo\boi\bid\bd *\b*d\bde\bef\bfe\ber\br_\b_c\bct\btx\bx)\b)
+    Called once from inside a test, to call defer_fn(defer_ctx) after the test
+    completes. This is typically used to eliminate a resource leak in tests
+    that terminate the test early.
+
+    NOTE: The deferred function is designed to run outside a test, and
+    therefore it must not call Ptest functions.
index e5eabc57a34c9fa365806934817384e3c3b0d440..580a0131d3e450b3c41b8469b0c1f7cec0d01d87 100644 (file)
@@ -186,6 +186,13 @@ postfix/sasl/, /var/lib/sasl2/ etc. See the output of postconf
 cyrus_sasl_config_path and/or the distribution-specific documentation to
 determine the expected location.
 
+Some Debian-based Postfix distributions patch Postfix to hardcode a non-default
+search path, making it impossible to set an alternate search path via the
+"cyrus_sasl_config_path" parameter. This is likely to be the case when the
+distribution documents a Postfix-specific path (e.g. /etc/postfix/sasl/) that
+is different from the default value of "cyrus_sasl_config_path" (which then is
+likely to be empty).
+
     N\bNo\bot\bte\be
 
     Cyrus SASL searches /usr/lib/sasl2/ first. If it finds the specified
index 46bbf165f51db6c2d1c955f8275d10c194c141ef..47a6fa310be39870635fcbbc6af864abfe31fe2b 100644 (file)
@@ -87,6 +87,7 @@ a delegated SMTPD access policy request:
     server_port=54321
     P\bPo\bos\bst\btf\bfi\bix\bx v\bve\ber\brs\bsi\bio\bon\bn 3\b3.\b.8\b8 a\ban\bnd\bd l\bla\bat\bte\ber\br:\b:
     compatibility_level=major.minor.patch
+    mail_version=3.8.0
     [empty line]
 
 Notes:
@@ -170,6 +171,10 @@ Notes:
     parameter value. It has the form major.minor.patch where minor and patch
     may be absent.
 
+  * The "mail_version" attribute corresponds to the mail_version parameter
+    value. It has the form major.minor.patch for stable releases, and
+    major.minor-yyyymmdd for unstable releases.
+
 The following is specific to SMTPD delegated policy requests:
 
   * Protocol names are ESMTP or SMTP.
index cc67947e17dce296777975b28c0c998372b57fc3..42943dea10b007567e2a235bb424263e82eb5dd4 100644 (file)
@@ -1,3 +1,59 @@
+Consolidate postscreen_dnsbl tests that differ only in data.
+
+Add a mock_server_test case for a non-matching request (error propagation
+check).
+
+DONE postscreen_dnsbl_test.c. The code structure is 
+
+void    psc_dnsbl_init(void) // one-time initialization
+
+int     psc_dnsbl_request(const char *client_addr,
+                                  void (*callback) (int, void *),
+                                  void *context)
+
+Calls LOCAL_CONNECT() which would need to be mocked (this calls
+unix_connect() on solaris 9+ and all other systems).  Returns a request
+index that must be passed to psc_dnsbl_retrieve().
+
+psc_dnsbl_retrieve() is called before the pregreet_wait timer expires when
+a client hangs up, when postscreen drops the client, or by the callback.
+the pregreet_wait timer expires, or by the callback.
+
+In the following paragraph, a direct call is a call that is not made
+through the events framework (for example, using event_request_timer()
+with a zero delay).
+
+The callback is called directly when the complete score for client_addr
+is known before the pregreet_wait timer expires.  The callback then
+directly calls psc_dnsbl_retrieve().
+
+Test structure:
+
+- We need a mock attribute server (in-process or in-child process) that
+responds to requests. Or do we? It could be all memory streams that read
+from prepared VSTRINGs
+
+How do we test a program that uses the events framework? 
+
+- Use a socketpair or pipe and write messages smaller than the buffer
+size, so that writes would not block (or use non-blocking writes to be
+informed when a write is too large). A pipe will accept 16384
+bytes on NetBSD 9.2 and OmniOS 5.11 (Solaris), and 65536 bytes
+on FreeBSD 13.0, macOS 11.6.1, and  Linux 5.8.15 / Fedora 33.
+With socketpairs the results vary.
+https://www.netmeister.org/blog/ipcbufs.html
+
+- Async mock client/server: create pipe; write request, and set up a
+read request that will wake up the mock server, compare serialized
+request against expectation, write prepared serialized reponse.
+
+
+================
+DONE Split documentation into PTEST_README and PMOCK_README
+
+PTEST_README: intro, links to all sections, simple example, more complex
+example, managing errors, managing logs, managing flows.
+
 TODO write a TEST_README that summarizes how to write simple tests
 that don't need custom PTEST_CASE fields (use the example in
 ptest_main.h), how to report test errors and how to require them,
@@ -7,8 +63,9 @@ and how to mock out dependencies (use example in pmock_expect_test.c).
 
 TODO document NO_MOCK_WRAPPERS in makedefs.
 
-TODO move PCRE tests to src/global, and either make them skippable 
-(#ifndef USE_DYNAMIC_MAPS) or support dynamic loading in tests.
+TODO make PCRE tests skippable (#ifndef USE_DYNAMIC_MAPS), or move
+them to src/global and implement support for dynamic loading
+in tests.
 
 DONE Need a way to SKIP tests, and report those in the summmary.
 
@@ -24,6 +81,8 @@ TODO Port haproxy_srvr.c, to use PTEST.
 
 TODO Test that eq_sockaddr() and eq_addrinfo compare all fields for equality.
 
+TODO Add the missing tests in myaddrinfo_test.c.
+
 ==============
 
 DONE Subtests. What would the API look like?
@@ -56,21 +115,21 @@ In-line code, single-macro alternative:
 
     RUN_TEST(t, name, {
        /* do actual test */
-   });
+    });
 
 Now we're talking!
 
-Both Linux gcc 11.3.1 and FreeBSD clang version 11.0.1 pre-process {}
-inside () without problems.
+Both Linux gcc 11.3.1 and FreeBSD clang version 11.0.1 have no
+problems with compiling compile code with {} inside a macro argument.
 
 indent complains about "Unbalanced parens" before the '(' and "Extra
 )" before the ')', but formats the code correctly.
 
-clang-format 12.0.1 handles {} inside (), but needs comments with /*
-clang-format off */ and /* clang-format on */ to avoid messing up the
-macro definition (it removes the '\' at the end of the lines).
+clang-format 12.0.1 handles {} inside a macro argument, but needs comments
+with /* clang-format off */ and /* clang-format on */ to avoid messing
+up the macro definition (it removes the '\' at the end of the lines).
 
-TODO: make sure that a subtest within a subtest propagates
+DONE: make sure that a subtest within a subtest propagates
 the number of subtests passed/failed.
 
 Assume that there always is a parent (the root is special).
index 12e8b031e08c1d6cc14b2aa3ca4632fb01a2e34e..6738649a9dff317c0582fded1e4cad9c4c518805 100644 (file)
@@ -9,6 +9,8 @@ Wish list:
        Scan Postfix code with github.com/googleprojectzero/weggli
        (depends on "rust").
 
+       Migrate masquerade_domains from ARGV to STRING_LIST.
+
        Enforce var_line_limit in util/attr_scan*c.
 
        Investigate clang-format compatibility compared to indent.
@@ -26,8 +28,6 @@ Wish list:
 
        WARN_IF_REJECT like prefix that disables the error counter increment.
 
-       Send the Postfix version in a policy server request.
-
        postscreen_dnsbl_sites is evaluated in the reverse order, breaking
        expectations when different reply patterns have different weights.
        We need a compatibility_level feature to correct this.
index 941551e9d7af07a86c8037656f181e7fff15e966..8f1a2845161064858726c8f0b0fb47c540d10e87 100644 (file)
@@ -108,16 +108,20 @@ decode:           root
 #               with the RFC 822 standard.
 # 
 #        /file/name
-#               Mail  is  appended  to /file/name. See local(8) for
-#               details of delivery to file.  Delivery is not  lim-
-#               ited  to regular files.  For example, to dispose of
-#               unwanted mail, deflect it to /dev/null.
+#               Mail  is appended to /file/name. For details on how
+#               a file is written see the sections  "EXTERNAL  FILE
+#               DELIVERY"  and  "DELIVERY  RIGHTS"  in the local(8)
+#               documentation.  Delivery is not limited to  regular
+#               files.   For  example, to dispose of unwanted mail,
+#               deflect it to /dev/null.
 # 
 #        |command
 #               Mail is piped into command. Commands  that  contain
 #               special  characters,  such as whitespace, should be
-#               enclosed between double quotes.  See  local(8)  for
-#               details of delivery to command.
+#               enclosed between double quotes. For details on  how
+#               a  command is executed see "EXTERNAL COMMAND DELIV-
+#               ERY" and "DELIVERY RIGHTS" in the local(8) documen-
+#               tation.
 # 
 #               When the command fails, a limited amount of command
 #               output is mailed back  to  the  sender.   The  file
@@ -218,18 +222,17 @@ decode:           root
 #               the recipient_delimiter is set to "-".
 # 
 #        recipient_delimiter (empty)
-#               The set of characters that can separate a user name
-#               from its extension (example: user+foo), or a  .for-
-#               ward  file  name from its extension (example: .for-
-#               ward+foo).
+#               The  set  of  characters that can separate an email
+#               address localpart, user name, or  a  .forward  file
+#               name from its extension.
 # 
 #        Available in Postfix version 2.3 and later:
 # 
 #        frozen_delivered_to (yes)
-#               Update the local(8) delivery agent's  idea  of  the
-#               Delivered-To:     address    (see    prepend_deliv-
-#               ered_header) only once, at the start of a  delivery
-#               attempt;  do  not  update the Delivered-To: address
+#               Update  the  local(8)  delivery agent's idea of the
+#               Delivered-To:    address    (see     prepend_deliv-
+#               ered_header)  only once, at the start of a delivery
+#               attempt; do not update  the  Delivered-To:  address
 #               while expanding aliases or .forward files.
 # 
 # STANDARDS
@@ -242,12 +245,12 @@ decode:           root
 #        postconf(5), configuration parameters
 # 
 # README FILES
-#        Use "postconf readme_directory" or  "postconf  html_direc-
+#        Use  "postconf  readme_directory" or "postconf html_direc-
 #        tory" to locate this information.
 #        DATABASE_README, Postfix lookup table overview
 # 
 # LICENSE
-#        The  Secure  Mailer  license must be distributed with this
+#        The Secure Mailer license must be  distributed  with  this
 #        software.
 # 
 # AUTHOR(S)
index 643a1f319cb0c5813b732135bdf23c1ef7456b3b..cda021929ce7a4e4db15fbb28cebd7e10d6b5b22 100644 (file)
@@ -303,6 +303,7 @@ $readme_directory/MEMCACHE_README:f:root:-:644
 $readme_directory/MILTER_README:f:root:-:644
 $readme_directory/MULTI_INSTANCE_README:f:root:-:644
 $readme_directory/MYSQL_README:f:root:-:644
+$readme_directory/PTEST_README:f:root:-:644
 $readme_directory/SMTPUTF8_README:f:root:-:644
 $readme_directory/SQLITE_README:f:root:-:644
 $readme_directory/NFS_README:f:root:-:644
@@ -364,6 +365,7 @@ $html_directory/MEMCACHE_README.html:f:root:-:644
 $html_directory/MILTER_README.html:f:root:-:644
 $html_directory/MULTI_INSTANCE_README.html:f:root:-:644
 $html_directory/MYSQL_README.html:f:root:-:644
+$html_directory/PTEST_README.html:f:root:-:644
 $html_directory/SMTPUTF8_README.html:f:root:-:644
 $html_directory/SQLITE_README.html:f:root:-:644
 $html_directory/NFS_README.html:f:root:-:644
diff --git a/postfix/html/PTEST_README.html b/postfix/html/PTEST_README.html
new file mode 100644 (file)
index 0000000..2cdb252
--- /dev/null
@@ -0,0 +1,599 @@
+<!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN"
+        "http://www.w3.org/TR/html4/loose.dtd">
+
+<html>
+
+<head>
+
+<title>Writing Postfix unit tests </title>
+
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+</head>
+
+<body>
+
+<h1><img src="postfix-logo.jpg" width="203" height="98" ALT="">Writing
+Postfix unit tests </h1>
+
+<hr>
+
+<h2> Overview </h2>
+
+<p> This document covers, Ptest, a simple unit test framework that
+was introduced with Postfix version 3.8. It is modeled after Go
+tests, with primitives such as <a href="PTEST_README.html#ptest_error">ptest_error</a>() and <a href="PTEST_README.html#ptest_fatal">ptest_fatal</a>() that
+report test failures, and <a href="PTEST_README.html#PTEST_RUN">PTEST_RUN</a>() that supports subtests. </p>
+
+<p> Ptest is light-weight compared to more powerful framweworks
+such as Gtest, but it avoids the need for adding a large Postfix
+dependency (a dependency that would not affect Postfix distributors,
+but developers only). </p>
+
+<ul>
+
+<li> <p> <a href="#simple_example"> Simple example </a> </p>
+
+<li> <p> <a href="#test_case_data"> Testing one function with
+TEST_CASE data </a> </p>
+
+<li> <p> <a href="#sub_tests"> Testing functions with subtests </a> </p>
+
+<li> <p> <a href="#style"> Suggestions for writing tests </a> </p>
+
+<li> <p> <a href="#api_reference"> Ptest API reference  </a> </p>
+
+</ul>
+
+<h2> <a name="simple_example"> Simple example </a> </h2>
+
+<p> Simple tests exercise one function under test, one scenario at
+a time.  Each scenario calls the function under test with good or
+bad inputs, and verifies that the function behaves as expected. The
+code in Postfix <tt>mymalloc_test.c</tt> file is a good example. </p>
+
+<p> After some <tt>#include</tt> statements, the file goes like
+this: </p>
+
+<blockquote>
+<pre>
+ 27 typedef struct PTEST_CASE {
+ 28     const char *testname;               /* Human-readable description */
+ 29     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+ 30 } PTEST_CASE;
+ 31 
+ 32 /* Test functions. */
+ 33 
+ 34 static void test_mymalloc_normal(PTEST_CTX *t, const PTEST_CASE *tp)
+ 35 {
+ 36     void   *ptr;
+ 37 
+ 38     ptr = mymalloc(100);
+ 39     myfree(ptr);
+ 40 }
+ 41 
+ 42 static void test_mymalloc_panic_too_small(PTEST_CTX *t, const PTEST_CASE *tp)
+ 43 {
+ 44     <a href="PTEST_README.html#expect_ptest_log_event">expect_ptest_log_event</a>(t, "panic: mymalloc: requested length 0");
+ 45     (void) mymalloc(0);
+ 46     <a href="PTEST_README.html#ptest_fatal">ptest_fatal</a>(t, "mymalloc(0) returned");
+ 47 }
+...     // Test functions for myrealloc(), mystrdup(), mymemdup().
+260
+261 static const PTEST_CASE ptestcases[] = {
+262     {"mymalloc + myfree normal case", test_mymalloc_normal,
+263     },
+264     {"mymalloc panic for too small request", test_mymalloc_panic_too_small,
+265     },
+...     // Test cases for myrealloc(), mystrdup(), mymemdup().
+306 };
+307 
+308 #include &lt;ptest_main.h&gt;
+</pre>
+</blockquote>
+
+<p> To run the test: </p>
+
+<blockquote>
+<pre>
+$ make test_mymalloc
+... compiler output...
+LD_LIBRARY_PATH=/path/to/postfix-source/lib ./mymalloc_test
+RUN  mymalloc + myfree normal case
+PASS mymalloc + myfree normal case
+RUN  mymalloc panic for too small request
+PASS mymalloc panic for too small request
+... results for myrealloc(), mystrdup(), mymemdup()...
+mymalloc_test: PASS: 22, SKIP: 0, FAIL: 0
+</pre>
+</blockquote>
+
+<p> This simple example already shows several key features of the ptest
+framework. </p>
+
+<ul>
+
+<li> <p> Each test is implemented as a separate function
+(<tt>test_mymalloc_normal()</tt>, <tt>test_mymalloc_panic_too_small()</tt>,
+and so on). </p>
+
+<li> <p> The first test verifies 'normal' behavior: it verifies that
+<tt>mymalloc()</tt> will allocate a small amount of memory, and that
+<tt>myfree()</tt> will accept the result from <tt>mymalloc()</tt>.
+When the test is run under a memory checker such as Valgrind, the
+memory checker will report no memory leak or other error. </p>
+
+<li> <p> The second test is more interesting. </p>
+
+<ul>
+
+<li> <p> The test verifies that <tt>mymalloc()</tt> will call
+<tt>msg_panic()</tt> when the requested amount of memory is too
+small. But in this test the <tt>msg_panic()</tt> call will not
+terminate the process like it normally would. The Ptest framework
+changes the control flow of <tt>msg_panic()</tt> and <tt>msg_fatal()</tt>
+such that these functions will terminate their test, instead of
+their process. </p>
+
+<li> <p> The <tt><a href="PTEST_README.html#expect_ptest_log_event">expect_ptest_log_event</a>()</tt> call sets up an
+expectation that <tt>msg_panic()</tt> will produce a specific error
+message; the test would fail if the expectation remains unsatisfied.
+</p>
+
+<li> <p> The <tt><a href="PTEST_README.html#ptest_fatal">ptest_fatal</a>()</tt> call at the end of the second
+test is not needed; this call can only be reached if <tt>mymalloc()</tt>
+does not call <tt>msg_panic()</tt>. But then the expected panic
+message will not be logged, and the test will fail anyway. </p>
+
+</ul>
+
+<li> <p> The <tt>ptestcases[]</tt> table near the end of the example
+contains for each test the name and a pointer to function. As we
+show in a later example, the <tt>ptestcases[]</tt> table can also
+contain test inputs and expectations. </p>
+
+<li> <p> The "<tt>#include &lt;ptest_main.h&gt;</tt>" at the end pulls
+in the code that iterates over the <tt>ptestcases[]</tt> table and
+logs progress.
+
+<li> <p> The test run output shows that the <tt>msg_panic()</tt>
+output in the second test is silenced; only output from unexpected
+<tt>msg_panic()</tt> or other unexpected <tt>msg(3)</tt> calls would
+show up in test run output. </p>
+
+</ul>
+
+<h2> <a name="test_case_data"> Testing one function with
+TEST_CASE data </a> </h2>
+
+<p> Often, we want to test a module that contains only one function. In
+that case we can store all the test inputs and expected results in the
+PTEST_CASE structure. </p>
+
+<p> The examples below are taken from the <tt>dict_union_test.c</tt>
+file which test the <tt>unionmap</tt> implementation in the file.
+<tt>dict_union.c</tt>. </p>
+
+<p> Background: a unionmap creates a union of tables. For example,
+the lookup table "<tt><a href="DATABASE_README.html#types">unionmap</a>:{<a href="DATABASE_README.html#types">inline</a>:{foo=one},<a href="DATABASE_README.html#types">inline</a>:{foo=two}}</tt>"
+will return ("<tt>one, two</tt>", DICT_STAT_SUCCESS) when queried
+with <tt>foo</tt>, and will return (NOTFOUND, DICT_STAT_SUCCESS)
+otherwise. </p>
+
+<p> First, we present the TEST_CASE structure with additional fields
+for inputs and expected results. </p>
+
+<blockquote>
+<pre>
+ 29 #define MAX_PROBE       5
+ 30 
+ 31 struct probe {
+ 32     const char *query;
+ 33     const char *want_value;
+ 34     int     want_error;
+ 35 };
+ 36 
+ 37 typedef struct PTEST_CASE {
+ 38     const char *testname;
+ 39     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+ 40     const char *type_name;
+ 41     const struct probe probes[MAX_PROBE];
+ 42 } PTEST_CASE;
+</pre>
+</blockquote>
+
+<p> In the PTEST_CASE structure above: </p>
+
+<ul>
+
+<li> <p> The <tt>testname</tt> and <tt>action</tt> fields are
+standard. We have seen these already in the simple example above.
+<p>
+
+<li> <p> The <tt>type_name</tt> field will contain the name of the table,
+for example <tt><a href="DATABASE_README.html#types">unionmap</a>:{<a href="DATABASE_README.html#types">static</a>:one,<a href="DATABASE_README.html#types">inline</a>:{foo=two}}</tt>. </p>
+
+<li> <p> The <tt>probes</tt> field contains a list of (query, expected
+result value, expected error code) that will be used to query the unionmap
+and to verify the result value and error code.
+</p>
+
+</ul>
+
+<p> Next we show the test data. Every test calls the same
+<tt>test_dict_union()</tt> function with a different <tt>unionmap</tt>
+configuration and with a list of queries with expected results. The
+implementation of that function follows after the test data. </p>
+
+<blockquote>
+<pre>
+ 78 static const PTEST_CASE ptestcases[] = {
+ 79     {
+ 80          /* testname */ "successful lookup: static map + inline map",
+ 81          /* action */ test_dict_union,
+ 82          /* type_name */ "<a href="DATABASE_README.html#types">unionmap</a>:{<a href="DATABASE_README.html#types">static</a>:one,<a href="DATABASE_README.html#types">inline</a>:{foo=two}}",
+ 83          /* probes */ {
+ 84             {"foo", "one,two", DICT_STAT_SUCCESS},
+ 85             {"bar", "one", DICT_STAT_SUCCESS},
+ 86         },
+ 87     }, {
+ 88          /* testname */ "error propagation: static map + fail map",
+ 89          /* action */ test_dict_union,
+ 90          /* type_name */ "<a href="DATABASE_README.html#types">unionmap</a>:{<a href="DATABASE_README.html#types">static</a>:one,<a href="DATABASE_README.html#types">fail</a>:fail}",
+ 91          /* probes */ {
+ 92             {"foo", 0, DICT_STAT_ERROR},
+ 93         },
+...
+102 };
+103 
+104 #include &lt;ptest_main.h&gt;
+</pre>
+</blockquote>
+
+<p> Finally, here is the <tt>test_dict_union()</tt> function that
+tests the <tt>unionmap</tt> implementation with a given configuration
+and test queries. </p>
+
+<blockquote>
+<pre>
+ 44 #define STR_OR_NULL(s)  ((s) ? (s) : "null")
+ 45 
+ 46 static void test_dict_union(PTEST_CTX *t, const struct PTEST_CASE *tp)
+ 47 {
+ 48     DICT   *dict;
+ 49     const struct probe *pp;
+ 50     const char *got_value;
+ 51     int     got_error;
+ 52 
+ 53     if ((dict = dict_open(tp->type_name, O_RDONLY, 0)) == 0)
+ 54         <a href="PTEST_README.html#ptest_fatal">ptest_fatal</a>(t, "dict_open(\"%s\", O_RDONLY, 0) failed: %m",
+ 55                     tp->type_name);
+ 56     for (pp = tp->probes; pp < tp->probes + MAX_PROBE && pp->query != 0; pp++) {
+ 57         got_value = dict_get(dict, pp->query);
+ 58         got_error = dict->error;
+ 59         if (got_value == 0 && pp->want_value == 0)
+ 60             continue;
+ 61         if (got_value == 0 || pp->want_value == 0) {
+ 62             <a href="PTEST_README.html#ptest_error">ptest_error</a>(t, "dict_get(dict, \"%s\"): got '%s', want '%s'",
+ 63                         pp->query, STR_OR_NULL(got_value),
+ 64                         STR_OR_NULL(pp->want_value));
+ 65             break;
+ 66         }
+ 67         if (strcmp(got_value, pp->want_value) != 0) {
+ 68             <a href="PTEST_README.html#ptest_error">ptest_error</a>(t, "dict_get(dict, \"%s\"): got '%s', want '%s'",
+ 69                         pp->query, got_value, pp->want_value);
+ 70         }
+ 71         if (got_error != pp->want_error)
+ 72             <a href="PTEST_README.html#ptest_error">ptest_error</a>(t, "dict_get(dict,\"%s\") <a href="error.8.html">error</a>: got %d, want %d",
+ 73                         pp->query, got_error, pp->want_error);
+ 74     }
+ 75     dict_free(dict);
+ 76 }
+</pre> 
+</blockquote>
+
+<p> A test run looks like this: </p>
+
+<blockquote>
+<pre>
+$ make test_dict_union
+...compiler output...
+LD_LIBRARY_PATH=/path/to/postfix-source/lib ./dict_union_test
+RUN  successful lookup: static map + inline map
+PASS successful lookup: static map + inline map
+RUN  error propagation: static map + fail map
+PASS error propagation: static map + fail map
+...
+dict_union_test: PASS: 3, SKIP: 0, FAIL: 0
+</pre>
+</blockquote>
+
+<h2> <a name="sub_tests"> Testing functions with subtests </a> </h2>
+
+<p> Sometimes it is not convenient to store test data in a PTEST_CASE
+structure. This can happen when converting an existing test into
+Ptest, or when the module under test contains multiple functions
+that need different kinds of test data. The solution is to create
+a <tt>_test.c</tt> file with the structure shown below. The example
+is based on code in <tt>map_search_test.c</tt> that was converted
+from an existing test into Ptest. </p>
+
+<ul>
+
+<li> <p> One PTEST_CASE structure definition without test data. </p>
+
+<pre>
+ 50 typedef struct PTEST_CASE {
+ 51     const char *testname;
+ 52     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+ 53 } PTEST_CASE;
+</pre>
+
+<li> <p> One test function for each module function that needs to
+be tested, and one table with test cases for that module function.
+In this case there is only one module function (<tt>map_search()</tt>)
+that needs to be tested, so there is only one test function
+(<tt>test_map_search()</tt>). </p>
+
+<pre>
+ 67 #define MAX_WANT_LOG    5
+ 68 
+ 69 static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE *unused)
+ 70 {
+ 71     /* Test cases with inputs and expected outputs. */
+ 72     struct test {
+ 73         const char *map_spec;
+ 74         int     want_return;            /* 0=fail, 1=success */
+ 75         const char *want_log[MAX_WANT_LOG];
+ 76         const char *want_map_type_name; /* 0 or match */
+ 77         const char *exp_search_order;   /* 0 or match */
+ 78     };
+ 79     static struct test test_cases[] = {
+ 80         { /* 0 */ "type", 0, {
+ 81                 "malformed map specification: 'type'",
+ 82                 "expected maptype:mapname instead of 'type'",
+ 83         }, 0},
+...        // ...other test cases...
+111     };
+</pre>
+
+<li> <p> In a test function, iterate over its table with test cases,
+using <tt><a href="PTEST_README.html#PTEST_RUN">PTEST_RUN</a>()</tt> to run each test case in its own subtest.
+</p>
+
+<pre>
+129     for (tp = test_cases; tp->map_spec; tp++) {
+130         vstring_sprintf(test_label, "test %d", (int) (tp - test_cases));
+131         <a href="PTEST_README.html#PTEST_RUN">PTEST_RUN</a>(t, STR(test_label), {
+132             for (cpp = tp->want_log; cpp < tp->want_log + MAX_WANT_LOG && *cpp; cpp++)
+133                 <a href="PTEST_README.html#expect_ptest_log_event">expect_ptest_log_event</a>(t, *cpp);
+134             map_search_from_create = map_search_create(tp->map_spec);
+...            // ...verify that the result is as expected...
+...            // ...use <a href="PTEST_README.html#ptest_return">ptest_return</a>() or <a href="PTEST_README.html#ptest_fatal">ptest_fatal</a>() to exit from a test...
+173         });
+174     }
+...
+178 }
+</pre>
+
+<li> <p> Create a <tt>ptestcases[]</tt> table to call each test
+function once, and include the Ptest main program. </p>
+
+<pre>
+183 static const PTEST_CASE ptestcases[] = {
+184     "test_map_search", test_map_search,
+185 };
+186 
+187 #include &lt;ptest_main.h&gt;
+</pre>
+
+</ul>
+
+<p> See the file <tt>map_search_test.c</tt> for a complete example.
+</p>
+
+<p> This is what a test run looks like: </p>
+
+<blockquote>
+<pre>
+$ make test_map_search
+...compiler output...
+LD_LIBRARY_PATH=/path/to/postfix-source/lib  ./map_search_test
+RUN  test_map_search
+RUN  test_map_search/test 0
+PASS test_map_search/test 0
+....
+PASS test_map_search
+map_search_test: PASS: 13, SKIP: 0, FAIL: 0
+</pre>
+</blockquote>
+
+<p> This shows that the subtest name is appended to the parent test
+name, formatted as <i>parent-name/child-name</i>. </p>
+
+<h2> <a name="style"> Suggestions for writing tests </a> </h2>
+
+<p> Ptest is loosely inspired on Go test, especially its top-level
+test functions and its methods <tt>T.run()</tt>, <tt>T.error()</tt>
+and <tt>T.fatal()</tt>. </p>
+
+<p> Suggestions for test style may look familiar to Go programmers:
+</p>
+
+<ul>
+
+<li> <p> Use variables named <tt>got_xxx</tt> and <tt>want_xxx</tt>,
+and when a test result is unexpected, log the discrepancy as "got
+&lt;what you got&gt;, want &lt;what you want&gt;".  </p>
+
+<li> <p> Report discrepancies with <tt><a href="PTEST_README.html#ptest_error">ptest_error</a>()</tt> if possible;
+use <tt><a href="PTEST_README.html#ptest_fatal">ptest_fatal</a>()</tt> only when continuing the test would
+produce nonsensical results. </p>
+
+<li> <p> Where it makes sense use a table with testcases and use
+<tt><a href="PTEST_README.html#PTEST_RUN">PTEST_RUN</a>()</tt> to run each testcase in its own subtest. </p>
+
+</ul>
+
+<p> Other suggestions: </p>
+
+<ul>
+
+<li> <p> Consider running tests under a memory checker such as
+Valgrind. Use <tt><a href="PTEST_README.html#ptest_defer">ptest_defer</a>()</tt> to avoid memory leaks when a
+test may terminate early. </p>
+
+<li> <p> Always test non-error and error cases, to cover all code
+paths in the function under test. </p>
+
+</ul>
+
+<h2> <a name="api_reference"> Ptest API reference </a> </h2>
+
+<ul>
+
+<li> <p> <a href="#managing_errors">Managing test errors</a>
+
+<li> <p> <a href="#managing_logs">Managing log events</a>
+
+<li> <p> <a href="#managing_flows">Managing test execution </a>
+
+</ul>
+
+<h2> <a name="managing_errors"> Managing test errors</a> </h2>
+
+<p> As one might expect, Ptest has support to flag unexpected test
+results as errors. </p>
+
+<dl>
+
+<dt> <b> <a name="ptest_error"> <tt>void ptest_error(PTEST_CTX *t,
+const char *format, ...)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to report an unexpected test result,
+and to flag the test as failed without terminating the test. This
+call can be ignored with <tt><a href="PTEST_README.html#expect_ptest_error">expect_ptest_error</a>()</tt>. <br> <br> </dd>
+
+<dt> <b> <a name="ptest_fatal"> <tt>void ptest_fatal(PTEST_CTX *t,
+const char *format, ...)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to report an unexpected test result,
+to flag the test as failed, and to terminate the test. This call
+cannot be ignored with <tt><a href="PTEST_README.html#expect_ptest_error">expect_ptest_error</a>()</tt>. </dd>
+
+</dl>
+
+<p> For convenience, Ptest has can also report non-error information.
+</p>
+
+<dl>
+
+<dt> <b> <a name="ptest_info"> <tt>void ptest_info(PTEST_CTX *t,
+const char *format, ...)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to report a non-error condition
+without terminating the test. This call cannot be ignored with
+<tt><a href="PTEST_README.html#expect_ptest_error">expect_ptest_error</a>()</tt>. </dd>
+
+</dl>
+
+<p> Finally, Ptest has support to test <tt><a href="PTEST_README.html#ptest_error">ptest_error</a>()</tt> itself,
+to verify that an intentional error is reported as expected. </p>
+
+<dl>
+
+<dt> <b> <a name="expect_ptest_error"> <tt>void
+expect_ptest_error(PTEST_CTX *t, const char *text)</tt> </a> </b>
+</dt>
+
+<dd> Called from inside a test to expect exactly one <tt><a href="PTEST_README.html#ptest_error">ptest_error</a>()</tt>
+call with the specified text, and to ignore that <tt><a href="PTEST_README.html#ptest_error">ptest_error</a>()</tt>
+call (i.e. don't flag the test as failed). To ignore multiple calls,
+call <tt><a href="PTEST_README.html#expect_ptest_error">expect_ptest_error</a>()</tt> multiple times. A test is flagged
+as failed when an expected error is not reported (and of course
+when an error is reported that is not expected with
+<tt><a href="PTEST_README.html#expect_ptest_error">expect_ptest_error</a>()</tt>). </dd>
+
+</dl>
+
+<h2> <a name="managing_logs"> Managing log events</a> </h2>
+
+<p> Ptest integrates with Postfix <tt>msg(3)</tt> logging. </p>
+
+<ul>
+
+<li> <p> Ptest changes the control flow of <tt>msg_fatal()</tt> and
+<tt>msg_panic()</tt>. When these functions are called during a test,
+Ptest flags a test as failed and terminates the test instead of the
+process. </p>
+
+<li> <p> Ptest silences the output from <tt>msg_info()</tt> and
+other <tt>msg(3)</tt> calls, and installs a log event listener tp
+monitor Postfix logging. </p>
+
+</ul>
+
+<p> Ptest provides the following API to manage log events: </p>
+
+<dl>
+
+<dt> <b> <a name="expect_ptest_log_event"> <tt>void
+expect_ptest_log_event(PTEST_CTX *t, const char *text)</tt> </a>
+</b> </dt>
+
+<dd> Called from inside a test to expect exactly one <tt>msg(3)</tt>
+call with the specified text. To expect multiple events, call
+<tt><a href="PTEST_README.html#expect_ptest_log_event">expect_ptest_log_event</a>()</tt> multiple times. A test is flagged
+as failed when expected text is not logged, or when text is logged
+that is not expected with <tt><a href="PTEST_README.html#expect_ptest_log_event">expect_ptest_log_event</a>()</tt>. </dd>
+
+</dl>
+
+<h2> <a name="managing_flows"> Managing test execution </a> </h2>
+
+<p> Ptest has a number of primitives that control test execution.
+</p>
+
+<dl>
+
+<dt> <b> <a name="PTEST_RUN"> <tt>void PTEST_RUN(PTEST_CTX *t, const
+char *test_name, { code in braces })</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to run the <tt>{ code in braces
+}</tt> in it own subtest environment. In the test progress report,
+the subtest name is appended to the parent test name, formatted as
+<i>parent-name/child-name</i>. <br> <br> NOTE: because <tt><a href="PTEST_README.html#PTEST_RUN">PTEST_RUN</a>()
+</tt> is a macro, the <tt>{ code in braces }</tt> must not contain
+a <tt>return</tt> statement; use <tt><a href="PTEST_README.html#ptest_return">ptest_return</a>()</tt> instead.
+It is OK for <tt>{ code in braces }</tt> to call a function that
+uses <tt>return</tt>. <br> <br> </dd>
+
+<dt> <b> <a name="ptest_skip"> <tt>NORETURN ptest_skip(PTEST_CTX
+*t)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to flag a test as skipped, and to
+terminate the test without terminating the process. Use this to
+disable tests that are not applicable for a specific system type
+or build configuration. <br> <br> </dd>
+
+<dt> <b> <a name="ptest_return"> <tt>NORETURN ptest_return(PTEST_CTX
+*t)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to terminate the test without
+terminating the process. <br> <br> </dd>
+
+<dt> <b> <a name="ptest_defer"> <tt>void ptest_defer(PTEST_CTX *t,
+void (*defer_fn)(void *), void  *defer_ctx)</tt> </a> </b> </dt>
+
+<dd> Called once from inside a test, to call <tt>defer_fn(defer_ctx)</tt>
+after the test completes. This is typically used to eliminate a
+resource leak in tests that terminate the test early. <br> <br>
+NOTE: The deferred function is designed to run outside a test, and
+therefore it must not call Ptest functions. </dd>
+
+</dl>
+
+</body>
+
+</html>
+
index 6520a6966f1de1b1fc4e7a50ae4c8a7cabc759b9..f3139166815c7bb39353d911bc513b46e0699bed 100644 (file)
@@ -280,6 +280,14 @@ configuration file in <code>/etc/postfix/sasl/</code>,
 <a href="postconf.5.html#cyrus_sasl_config_path">cyrus_sasl_config_path</a></code> and/or the distribution-specific
 documentation to determine the expected location. </p> </li>
 
+<li> <p> Some Debian-based Postfix distributions patch Postfix to
+hardcode a non-default search path, making it impossible to set an
+alternate search path via the "<a href="postconf.5.html#cyrus_sasl_config_path">cyrus_sasl_config_path</a>" parameter.  This
+is likely to be the case when the distribution documents a
+Postfix-specific path (e.g. <code>/etc/postfix/sasl/</code>) that is
+different from the default value of "<a href="postconf.5.html#cyrus_sasl_config_path">cyrus_sasl_config_path</a>" (which
+then is likely to be empty). </p> </li>
+
 </ul>
 
 <blockquote>
index aaa5218ed128fc0fb088719167757af73d973c9b..ba73f00585a3a61af21b3723b83e86dba27f5af3 100644 (file)
@@ -118,6 +118,7 @@ server_address=10.3.2.1
 server_port=54321
 <b>Postfix version 3.8 and later:</b>
 <a href="postconf.5.html#compatibility_level">compatibility_level</a>=<i>major</i>.<i>minor</i>.<i>patch</i>
+<a href="postconf.5.html#mail_version">mail_version</a>=3.8.0
 [empty line]
 </pre>
 </blockquote>
@@ -220,6 +221,12 @@ server_port=54321
    <i>major</i>.<i>minor</i>.<i>patch</i> where <i>minor</i> and
    <i>patch</i> may be absent. </p>
 
+   <li> <p> The "<a href="postconf.5.html#mail_version">mail_version</a>" attribute corresponds to the
+   <a href="postconf.5.html#mail_version">mail_version</a> parameter value. It has the form
+   <i>major</i>.<i>minor</i>.<i>patch</i> for stable releases, and
+   <i>major</i>.<i>minor</i>-<i>yyyymmdd</i> for unstable releases.
+   </p>
+
 </ul>
 
 <p> The following is specific to SMTPD delegated policy requests:
index e7d5b663b437013ab557e022ce51880c4ab472e3..68aa58a516c7fc866f9a39aa5beafc0b331ef440 100644 (file)
@@ -67,38 +67,41 @@ ALIASES(5)                                                          ALIASES(5)
               <a href="https://tools.ietf.org/html/rfc822">822</a> standard.
 
        <i>/file/name</i>
-              Mail  is  appended  to  <i>/file/name</i>.  See <a href="local.8.html"><b>local</b>(8)</a> for details of
-              delivery to file.  Delivery is not  limited  to  regular  files.
-              For  example,  to  dispose  of  unwanted  mail,  deflect  it  to
-              <b>/dev/null</b>.
+              Mail  is  appended  to  <i>/file/name</i>. For details on how a file is
+              written see the sections "EXTERNAL FILE DELIVERY" and  "DELIVERY
+              RIGHTS"  in the <a href="local.8.html"><b>local</b>(8)</a> documentation.  Delivery is not limited
+              to regular files.  For example, to  dispose  of  unwanted  mail,
+              deflect it to <b>/dev/null</b>.
 
        |<i>command</i>
-              Mail is piped into <i>command</i>. Commands that contain special  char-
-              acters,  such  as  whitespace, should be enclosed between double
-              quotes. See <a href="local.8.html"><b>local</b>(8)</a> for details of delivery to command.
-
-              When the command fails, a limited amount of  command  output  is
-              mailed  back  to  the  sender.  The file <b>/usr/include/sysexits.h</b>
-              defines the expected exit status codes. For example, use  <b>"|exit</b>
-              <b>67"</b>  to simulate a "user unknown" error, and <b>"|exit 0"</b> to imple-
+              Mail  is piped into <i>command</i>. Commands that contain special char-
+              acters, such as whitespace, should be  enclosed  between  double
+              quotes.  For  details on how a command is executed see "EXTERNAL
+              COMMAND DELIVERY" and "DELIVERY RIGHTS" in the <a href="local.8.html"><b>local</b>(8)</a> documen-
+              tation.
+
+              When  the  command  fails, a limited amount of command output is
+              mailed back to the  sender.   The  file  <b>/usr/include/sysexits.h</b>
+              defines  the expected exit status codes. For example, use <b>"|exit</b>
+              <b>67"</b> to simulate a "user unknown" error, and <b>"|exit 0"</b> to  imple-
               ment an expensive black hole.
 
        <b>:include:</b><i>/file/name</i>
-              Mail is sent to the  destinations  listed  in  the  named  file.
-              Lines  in <b>:include:</b> files have the same syntax as the right-hand
+              Mail  is  sent  to  the  destinations  listed in the named file.
+              Lines in <b>:include:</b> files have the same syntax as the  right-hand
               side of alias entries.
 
-              A destination can be any destination that is described  in  this
-              manual  page.  However, delivery to "|<i>command</i>" and <i>/file/name</i> is
-              disallowed by default. To enable,  edit  the  <b><a href="postconf.5.html#allow_mail_to_commands">allow_mail_to_com</a>-</b>
+              A  destination  can be any destination that is described in this
+              manual page. However, delivery to "|<i>command</i>" and  <i>/file/name</i>  is
+              disallowed  by  default.  To enable, edit the <b><a href="postconf.5.html#allow_mail_to_commands">allow_mail_to_com</a>-</b>
               <b><a href="postconf.5.html#allow_mail_to_commands">mands</a></b> and <b><a href="postconf.5.html#allow_mail_to_files">allow_mail_to_files</a></b> configuration parameters.
 
 <b>ADDRESS EXTENSION</b>
-       When  alias database search fails, and the recipient localpart contains
-       the optional  recipient  delimiter  (e.g.,  <i>user+foo</i>),  the  search  is
+       When alias database search fails, and the recipient localpart  contains
+       the  optional  recipient  delimiter  (e.g.,  <i>user+foo</i>),  the  search is
        repeated for the unextended address (e.g., <i>user</i>).
 
-       The   <b><a href="postconf.5.html#propagate_unmatched_extensions">propagate_unmatched_extensions</a></b>   parameter  controls  whether  an
+       The  <b><a href="postconf.5.html#propagate_unmatched_extensions">propagate_unmatched_extensions</a></b>  parameter  controls   whether   an
        unmatched address extension (<i>+foo</i>) is propagated to the result of table
        lookup.
 
@@ -107,9 +110,9 @@ ALIASES(5)                                                          ALIASES(5)
        before database lookup.
 
 <b>REGULAR EXPRESSION TABLES</b>
-       This section describes how the table lookups change when the  table  is
-       given  in the form of regular expressions. For a description of regular
-       expression lookup table syntax, see <a href="regexp_table.5.html"><b>regexp_table</b>(5)</a>  or  <a href="pcre_table.5.html"><b>pcre_table</b>(5)</a>.
+       This  section  describes how the table lookups change when the table is
+       given in the form of regular expressions. For a description of  regular
+       expression  lookup  table syntax, see <a href="regexp_table.5.html"><b>regexp_table</b>(5)</a> or <a href="pcre_table.5.html"><b>pcre_table</b>(5)</a>.
        NOTE: these formats do not use ":" at the end of a pattern.
 
        Each regular expression is applied to the entire search string. Thus, a
@@ -122,21 +125,21 @@ ALIASES(5)                                                          ALIASES(5)
        reasons there is no support for <b>$1</b>, <b>$2</b> etc. substring interpolation.
 
 <b>SECURITY</b>
-       The <a href="local.8.html"><b>local</b>(8)</a> delivery agent disallows regular  expression  substitution
+       The  <a href="local.8.html"><b>local</b>(8)</a>  delivery agent disallows regular expression substitution
        of $1 etc. in <b><a href="postconf.5.html#alias_maps">alias_maps</a></b>, because that would open a security hole.
 
-       The  <a href="local.8.html"><b>local</b>(8)</a>  delivery  agent will silently ignore requests to use the
-       <a href="proxymap.8.html"><b>proxymap</b>(8)</a> server within <b><a href="postconf.5.html#alias_maps">alias_maps</a></b>. Instead it will  open  the  table
+       The <a href="local.8.html"><b>local</b>(8)</a> delivery agent will silently ignore requests  to  use  the
+       <a href="proxymap.8.html"><b>proxymap</b>(8)</a>  server  within  <b><a href="postconf.5.html#alias_maps">alias_maps</a></b>. Instead it will open the table
        directly.  Before Postfix version 2.2, the <a href="local.8.html"><b>local</b>(8)</a> delivery agent will
        terminate with a fatal error.
 
 <b>CONFIGURATION PARAMETERS</b>
-       The following <a href="postconf.5.html"><b>main.cf</b></a> parameters are  especially  relevant.   The  text
-       below  provides  only  a  parameter  summary.  See <a href="postconf.5.html"><b>postconf</b>(5)</a> for more
+       The  following  <a href="postconf.5.html"><b>main.cf</b></a>  parameters  are especially relevant.  The text
+       below provides only a  parameter  summary.  See  <a href="postconf.5.html"><b>postconf</b>(5)</a>  for  more
        details including examples.
 
        <b><a href="postconf.5.html#alias_database">alias_database</a> (see 'postconf -d' output)</b>
-              The alias databases for <a href="local.8.html"><b>local</b>(8)</a> delivery that are updated  with
+              The  alias databases for <a href="local.8.html"><b>local</b>(8)</a> delivery that are updated with
               "<b>newaliases</b>" or with "<b>sendmail -bi</b>".
 
        <b><a href="postconf.5.html#alias_maps">alias_maps</a> (see 'postconf -d' output)</b>
@@ -149,30 +152,30 @@ ALIASES(5)                                                          ALIASES(5)
               Restrict <a href="local.8.html"><b>local</b>(8)</a> mail delivery to external files.
 
        <b><a href="postconf.5.html#expand_owner_alias">expand_owner_alias</a> (no)</b>
-              When   delivering   to   an   alias   "<i>aliasname</i>"  that  has  an
+              When  delivering  to  an   alias   "<i>aliasname</i>"   that   has   an
               "owner-<i>aliasname</i>"  companion  alias,  set  the  envelope  sender
               address to the expansion of the "owner-<i>aliasname</i>" alias.
 
        <b><a href="postconf.5.html#propagate_unmatched_extensions">propagate_unmatched_extensions</a> (canonical, virtual)</b>
-              What  address  lookup  tables copy an address extension from the
+              What address lookup tables copy an address  extension  from  the
               lookup key to the lookup result.
 
        <b><a href="postconf.5.html#owner_request_special">owner_request_special</a> (yes)</b>
-              Enable special  treatment  for  owner-<i>listname</i>  entries  in  the
+              Enable  special  treatment  for  owner-<i>listname</i>  entries  in the
               <a href="aliases.5.html"><b>aliases</b>(5)</a>  file,  and  don't  split  owner-<i>listname</i>  and  <i>list-</i>
-              <i>name</i>-request address localparts when the <a href="postconf.5.html#recipient_delimiter">recipient_delimiter</a>  is
+              <i>name</i>-request  address localparts when the <a href="postconf.5.html#recipient_delimiter">recipient_delimiter</a> is
               set to "-".
 
        <b><a href="postconf.5.html#recipient_delimiter">recipient_delimiter</a> (empty)</b>
-              The  set of characters that can separate an email address local-
+              The set of characters that can separate an email address  local-
               part, user name, or a .forward file name from its extension.
 
        Available in Postfix version 2.3 and later:
 
        <b><a href="postconf.5.html#frozen_delivered_to">frozen_delivered_to</a> (yes)</b>
-              Update the <a href="local.8.html"><b>local</b>(8)</a> delivery agent's idea of  the  Delivered-To:
-              address  (see  <a href="postconf.5.html#prepend_delivered_header">prepend_delivered_header</a>) only once, at the start
-              of a delivery attempt; do not update the  Delivered-To:  address
+              Update  the  <a href="local.8.html"><b>local</b>(8)</a> delivery agent's idea of the Delivered-To:
+              address (see <a href="postconf.5.html#prepend_delivered_header">prepend_delivered_header</a>) only once, at  the  start
+              of  a  delivery attempt; do not update the Delivered-To: address
               while expanding aliases or .forward files.
 
 <b>STANDARDS</b>
index 2fd0c1dc22a8291b74204dc4a5ee0139ecf8fe31..da6306ef059cf8e1607ac7bf68734db48ea1540d 100644 (file)
@@ -212,6 +212,14 @@ Recipients </a>
 
 </ul>
 
+<p><strong> For maintainers and contributors </strong></p>
+
+<ul>
+
+<li> <a href="PTEST_README.html"> Writing Postfix unit tests </a>
+
+</ul>
+
 </td>
 
 </table>
index 4d9b705feb26c0913c402160e7f4242741549fb5..03f0ddba9b8cc987e9d7bbbf64f9a0c333ae440e 100644 (file)
@@ -8609,13 +8609,16 @@ the file is read). </p>
 <DT><b><a name="postscreen_dnsbl_sites">postscreen_dnsbl_sites</a>
 (default: empty)</b></DT><DD>
 
-<p>Optional list of DNS allow/denylist domains, filters and weight
+<p>Optional list of patterns with DNS allow/denylist domains, filters
+and weight
 factors. When the list is non-empty, the <a href="dnsblog.8.html">dnsblog(8)</a> daemon will
-query these domains with the IP addresses of remote SMTP clients,
+query these domains with the reversed IP addresses of remote SMTP
+clients,
 and <a href="postscreen.8.html">postscreen(8)</a> will update an SMTP client's DNSBL score with
-each non-error reply. </p>
+each non-error reply as described below. </p>
 
-<p> Caution: when postscreen rejects mail, it replies with the DNSBL
+<p> Caution: when postscreen rejects mail, its SMTP response contains
+the DNSBL
 domain name. Use the <a href="postconf.5.html#postscreen_dnsbl_reply_map">postscreen_dnsbl_reply_map</a> feature to hide
 "password" information in DNSBL domain names. </p>
 
@@ -8623,26 +8626,25 @@ domain name. Use the <a href="postconf.5.html#postscreen_dnsbl_reply_map">postsc
 specified with <a href="postconf.5.html#postscreen_dnsbl_threshold">postscreen_dnsbl_threshold</a>, <a href="postscreen.8.html">postscreen(8)</a> can drop
 the connection with the remote SMTP client. </p>
 
-<p> Specify a list of domain=filter*weight entries, separated by
+<p> Specify a list of domain=filter*weight patterns, separated by
 comma or whitespace.  </p>
 
 <ul>
 
-<li> <p> When no "=filter" is specified, <a href="postscreen.8.html">postscreen(8)</a> will use any
-non-error DNSBL reply.  Otherwise, <a href="postscreen.8.html">postscreen(8)</a> uses only DNSBL
-replies that match the filter. The filter has the form d.d.d.d,
+<li> <p> When a pattern specifies no "=filter", <a href="postscreen.8.html">postscreen(8)</a> will
+use any non-error DNSBL query result.  Otherwise, <a href="postscreen.8.html">postscreen(8)</a>
+will use only DNSBL
+query results that match the filter. The filter has the form d.d.d.d,
 where each d is a number, or a pattern inside [] that contains one
 or more ";"-separated numbers or number..number ranges.  </p>
 
-<li> <p> When no "*weight" is specified, <a href="postscreen.8.html">postscreen(8)</a> increments
-the remote SMTP client's DNSBL score by 1.  Otherwise, the weight must be
-an integral number, and <a href="postscreen.8.html">postscreen(8)</a> adds the specified weight to
-the remote SMTP client's DNSBL score.  Specify a negative number for
-allowlisting.  </p>
+<li> <p> When a pattern specifies no "*weight", the weight of the
+pattern is 1.  Otherwise, the weight must be an integral number.
+Specify a negative number for allowlisting.  </p>
 
-<li> <p> When one <a href="postconf.5.html#postscreen_dnsbl_sites">postscreen_dnsbl_sites</a> entry produces multiple
-DNSBL responses, <a href="postscreen.8.html">postscreen(8)</a> applies the weight at most once.
-</p>
+<li> <p> When a pattern matches one or more DNSBL query results,
+<a href="postscreen.8.html">postscreen(8)</a> adds that pattern's weight once to the remote SMTP
+client's DNSBL score. </p>
 
 </ul>
 
index 628b5d75d095a6c6d768adcf5ac60d5bf2173ca7..a5da9069e4b20cf67c4818f9301be164b446f0de 100644 (file)
@@ -71,14 +71,17 @@ The \fIvalue\fR contains one or more of the following:
 Mail is forwarded to \fIaddress\fR, which is compatible
 with the RFC 822 standard.
 .IP \fI/file/name\fR
-Mail is appended to \fI/file/name\fR. See \fBlocal\fR(8)
-for details of delivery to file.
+Mail is appended to \fI/file/name\fR. For details on how a
+file is written see the sections "EXTERNAL FILE DELIVERY"
+and "DELIVERY RIGHTS" in the \fBlocal\fR(8) documentation.
 Delivery is not limited to regular files.  For example, to dispose
 of unwanted mail, deflect it to \fB/dev/null\fR.
 .IP "|\fIcommand\fR"
-Mail is piped into \fIcommand\fR. Commands that contain special
-characters, such as whitespace, should be enclosed between double
-quotes. See \fBlocal\fR(8) for details of delivery to command.
+Mail is piped into \fIcommand\fR. Commands that contain
+special characters, such as whitespace, should be enclosed
+between double quotes. For details on how a command is
+executed see "EXTERNAL COMMAND DELIVERY" and "DELIVERY
+RIGHTS" in the \fBlocal\fR(8) documentation.
 .sp
 When the command fails, a limited amount of command output is
 mailed back to the sender.  The file \fB/usr/include/sysexits.h\fR
index 1bdbc9cefd1f4ab8afa2ee9e710e8e95d0928ad5..289f0de88f557e605428e329e7b370e6b4793c94 100644 (file)
@@ -5441,13 +5441,16 @@ Example:
 .PP
 This feature is available in Postfix 2.8.
 .SH postscreen_dnsbl_sites (default: empty)
-Optional list of DNS allow/denylist domains, filters and weight
+Optional list of patterns with DNS allow/denylist domains, filters
+and weight
 factors. When the list is non\-empty, the \fBdnsblog\fR(8) daemon will
-query these domains with the IP addresses of remote SMTP clients,
+query these domains with the reversed IP addresses of remote SMTP
+clients,
 and \fBpostscreen\fR(8) will update an SMTP client's DNSBL score with
-each non\-error reply.
+each non\-error reply as described below.
 .PP
-Caution: when postscreen rejects mail, it replies with the DNSBL
+Caution: when postscreen rejects mail, its SMTP response contains
+the DNSBL
 domain name. Use the postscreen_dnsbl_reply_map feature to hide
 "password" information in DNSBL domain names.
 .PP
@@ -5455,23 +5458,23 @@ When a client's score is equal to or greater than the threshold
 specified with postscreen_dnsbl_threshold, \fBpostscreen\fR(8) can drop
 the connection with the remote SMTP client.
 .PP
-Specify a list of domain=filter*weight entries, separated by
+Specify a list of domain=filter*weight patterns, separated by
 comma or whitespace.
 .IP \(bu
-When no "=filter" is specified, \fBpostscreen\fR(8) will use any
-non\-error DNSBL reply.  Otherwise, \fBpostscreen\fR(8) uses only DNSBL
-replies that match the filter. The filter has the form d.d.d.d,
+When a pattern specifies no "=filter", \fBpostscreen\fR(8) will
+use any non\-error DNSBL query result.  Otherwise, \fBpostscreen\fR(8)
+will use only DNSBL
+query results that match the filter. The filter has the form d.d.d.d,
 where each d is a number, or a pattern inside [] that contains one
 or more ";"\-separated numbers or number..number ranges.
 .IP \(bu
-When no "*weight" is specified, \fBpostscreen\fR(8) increments
-the remote SMTP client's DNSBL score by 1.  Otherwise, the weight must be
-an integral number, and \fBpostscreen\fR(8) adds the specified weight to
-the remote SMTP client's DNSBL score.  Specify a negative number for
-allowlisting.
+When a pattern specifies no "*weight", the weight of the
+pattern is 1.  Otherwise, the weight must be an integral number.
+Specify a negative number for allowlisting.
 .IP \(bu
-When one postscreen_dnsbl_sites entry produces multiple
-DNSBL responses, \fBpostscreen\fR(8) applies the weight at most once.
+When a pattern matches one or more DNSBL query results,
+\fBpostscreen\fR(8) adds that pattern's weight once to the remote SMTP
+client's DNSBL score.
 .br
 .PP
 Examples:
index 2b1ad8b2ad76dd51f319bf2c82a46840993d1c6a..6f262a5da9f6c7e3bb9ce2434ec499be0f1f9b78 100755 (executable)
@@ -1171,6 +1171,18 @@ while (<>) {
     s/(ftp:\/\/[^ ,"\(\)]*[^ ,"\(\):;!?.])/<a href="$1">$1<\/a>/;
     s/\bRFC\s*([1-9]\d*)/<a href="https:\/\/tools.ietf.org\/html\/rfc$1">$&<\/a>/g;
 
+    # Ptest hyperlinks
+
+    s;\bptest_error\b;<a href="PTEST_README.html#$&">$&</a>;g;
+    s;\bptest_fatal\b;<a href="PTEST_README.html#$&">$&</a>;g;
+    s;\bptest_info\b;<a href="PTEST_README.html#$&">$&</a>;g;
+    s;\bexpect_ptest_error\b;<a href="PTEST_README.html#$&">$&</a>;g;
+    s;\bexpect_ptest_log_event\b;<a href="PTEST_README.html#$&">$&</a>;g;
+    s;\bPTEST_RUN\b;<a href="PTEST_README.html#$&">$&</a>;g;
+    s;\bptest_skip\b;<a href="PTEST_README.html#$&">$&</a>;g;
+    s;\bptest_return\b;<a href="PTEST_README.html#$&">$&</a>;g;
+    s;\bptest_defer\b;<a href="PTEST_README.html#$&">$&</a>;g;
+
     # Split README/RFC/parameter/restriction hyperlinks that span line breaks
 
     s/(<a href="[^"]*">)([-A-Za-z0-9_]*)\b([-<\/bB>]*\n *[<bB>]*)\b([-A-Za-z0-9_]*)(<\/a>)/$1$2$5$3$1$4$5/;
index 511bd4448b97774b199c2b1f267ba4c2ce2b1972..f44a1e39fa521bdd80c0ff784c45f881dc0e48ad 100644 (file)
@@ -37,6 +37,7 @@ HTML  = ../html/ADDRESS_CLASS_README.html \
        ../html/PGSQL_README.html \
        ../html/POSTSCREEN_3_5_README.html \
        ../html/POSTSCREEN_README.html \
+       ../html/PTEST_README.html \
        ../html/QSHAPE_README.html \
        ../html/RESTRICTION_CLASS_README.html ../html/SASL_README.html \
        ../html/SCHEDULER_README.html ../html/SMTPD_ACCESS_README.html \
@@ -85,6 +86,7 @@ README        = ../README_FILES/ADDRESS_CLASS_README \
        ../README_FILES/PGSQL_README \
        ../README_FILES/POSTSCREEN_3_5_README \
        ../README_FILES/POSTSCREEN_README \
+       ../README_FILES/PTEST_README \
        ../README_FILES/QSHAPE_README \
        ../README_FILES/RESTRICTION_CLASS_README \
        ../README_FILES/SASL_README ../README_FILES/SCHEDULER_README \
@@ -267,6 +269,9 @@ clobber:
 ../html/POSTSCREEN_README.html: POSTSCREEN_README.html
        $(DETAB) $? | $(POSTLINK) >$@
 
+../html/PTEST_README.html: PTEST_README.html
+       $(DETAB) $? | $(POSTLINK) >$@
+
 ../html/QMQP_README.html: QMQP_README.html
        $(DETAB) $? | $(POSTLINK) >$@
 
@@ -447,6 +452,9 @@ clobber:
 ../README_FILES/POSTSCREEN_README: POSTSCREEN_README.html
        $(DETAB) $? | $(HT2READ) >$@
 
+../README_FILES/PTEST_README: PTEST_README.html
+       $(DETAB) $? | $(HT2READ) >$@
+
 ../README_FILES/QMQP_README: QMQP_README.html
        $(DETAB) $? | $(HT2READ) >$@
 
diff --git a/postfix/proto/PTEST_README.html b/postfix/proto/PTEST_README.html
new file mode 100644 (file)
index 0000000..56323de
--- /dev/null
@@ -0,0 +1,599 @@
+<!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN"
+        "http://www.w3.org/TR/html4/loose.dtd">
+
+<html>
+
+<head>
+
+<title>Writing Postfix unit tests </title>
+
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+</head>
+
+<body>
+
+<h1><img src="postfix-logo.jpg" width="203" height="98" ALT="">Writing
+Postfix unit tests </h1>
+
+<hr>
+
+<h2> Overview </h2>
+
+<p> This document covers Ptest, a simple unit test framework that
+was introduced with Postfix version 3.8. It is modeled after Go
+tests, with primitives such as ptest_error() and ptest_fatal() that
+report test failures, and PTEST_RUN() that supports subtests. </p>
+
+<p> Ptest is light-weight compared to more powerful frameworks
+such as Gtest, but it avoids the need for adding a large Postfix
+dependency (a dependency that would not affect Postfix distributors,
+but developers only). </p>
+
+<ul>
+
+<li> <p> <a href="#simple_example"> Simple example </a> </p>
+
+<li> <p> <a href="#test_case_data"> Testing one function with
+TEST_CASE data </a> </p>
+
+<li> <p> <a href="#sub_tests"> Testing functions with subtests </a> </p>
+
+<li> <p> <a href="#style"> Suggestions for writing tests </a> </p>
+
+<li> <p> <a href="#api_reference"> Ptest API reference  </a> </p>
+
+</ul>
+
+<h2> <a name="simple_example"> Simple example </a> </h2>
+
+<p> Simple tests exercise one function under test, one scenario at
+a time.  Each scenario calls the function under test with good or
+bad inputs, and verifies that the function behaves as expected. The
+code in Postfix <tt>mymalloc_test.c</tt> file is a good example. </p>
+
+<p> After some <tt>#include</tt> statements, the file goes like
+this: </p>
+
+<blockquote>
+<pre>
+ 27 typedef struct PTEST_CASE {
+ 28     const char *testname;               /* Human-readable description */
+ 29     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+ 30 } PTEST_CASE;
+ 31 
+ 32 /* Test functions. */
+ 33 
+ 34 static void test_mymalloc_normal(PTEST_CTX *t, const PTEST_CASE *tp)
+ 35 {
+ 36     void   *ptr;
+ 37 
+ 38     ptr = mymalloc(100);
+ 39     myfree(ptr);
+ 40 }
+ 41 
+ 42 static void test_mymalloc_panic_too_small(PTEST_CTX *t, const PTEST_CASE *tp)
+ 43 {
+ 44     expect_ptest_log_event(t, "panic: mymalloc: requested length 0");
+ 45     (void) mymalloc(0);
+ 46     ptest_fatal(t, "mymalloc(0) returned");
+ 47 }
+...     // Test functions for myrealloc(), mystrdup(), mymemdup().
+260
+261 static const PTEST_CASE ptestcases[] = {
+262     {"mymalloc + myfree normal case", test_mymalloc_normal,
+263     },
+264     {"mymalloc panic for too small request", test_mymalloc_panic_too_small,
+265     },
+...     // Test cases for myrealloc(), mystrdup(), mymemdup().
+306 };
+307 
+308 #include &lt;ptest_main.h&gt;
+</pre>
+</blockquote>
+
+<p> To run the test: </p>
+
+<blockquote>
+<pre>
+$ make test_mymalloc
+... compiler output...
+LD_LIBRARY_PATH=/path/to/postfix-source/lib ./mymalloc_test
+RUN  mymalloc + myfree normal case
+PASS mymalloc + myfree normal case
+RUN  mymalloc panic for too small request
+PASS mymalloc panic for too small request
+... results for myrealloc(), mystrdup(), mymemdup()...
+mymalloc_test: PASS: 22, SKIP: 0, FAIL: 0
+</pre>
+</blockquote>
+
+<p> This simple example already shows several key features of the ptest
+framework. </p>
+
+<ul>
+
+<li> <p> Each test is implemented as a separate function
+(<tt>test_mymalloc_normal()</tt>, <tt>test_mymalloc_panic_too_small()</tt>,
+and so on). </p>
+
+<li> <p> The first test verifies 'normal' behavior: it verifies that
+<tt>mymalloc()</tt> will allocate a small amount of memory, and that
+<tt>myfree()</tt> will accept the result from <tt>mymalloc()</tt>.
+When the test is run under a memory checker such as Valgrind, the
+memory checker will report no memory leak or other error. </p>
+
+<li> <p> The second test is more interesting. </p>
+
+<ul>
+
+<li> <p> The test verifies that <tt>mymalloc()</tt> will call
+<tt>msg_panic()</tt> when the requested amount of memory is too
+small. But in this test the <tt>msg_panic()</tt> call will not
+terminate the process like it normally would. The Ptest framework
+changes the control flow of <tt>msg_panic()</tt> and <tt>msg_fatal()</tt>
+such that these functions will terminate their test, instead of
+their process. </p>
+
+<li> <p> The <tt>expect_ptest_log_event()</tt> call sets up an
+expectation that <tt>msg_panic()</tt> will produce a specific error
+message; the test would fail if the expectation remains unsatisfied.
+</p>
+
+<li> <p> The <tt>ptest_fatal()</tt> call at the end of the second
+test is not needed; this call can only be reached if <tt>mymalloc()</tt>
+does not call <tt>msg_panic()</tt>. But then the expected panic
+message will not be logged, and the test will fail anyway. </p>
+
+</ul>
+
+<li> <p> The <tt>ptestcases[]</tt> table near the end of the example
+contains for each test the name and a pointer to function. As we
+show in a later example, the <tt>ptestcases[]</tt> table can also
+contain test inputs and expectations. </p>
+
+<li> <p> The "<tt>#include &lt;ptest_main.h&gt;</tt>" at the end pulls
+in the code that iterates over the <tt>ptestcases[]</tt> table and
+logs progress.
+
+<li> <p> The test run output shows that the <tt>msg_panic()</tt>
+output in the second test is silenced; only output from unexpected
+<tt>msg_panic()</tt> or other unexpected <tt>msg(3)</tt> calls would
+show up in test run output. </p>
+
+</ul>
+
+<h2> <a name="test_case_data"> Testing one function with
+TEST_CASE data </a> </h2>
+
+<p> Often, we want to test a module that contains only one function. In
+that case we can store all the test inputs and expected results in the
+PTEST_CASE structure. </p>
+
+<p> The examples below are taken from the <tt>dict_union_test.c</tt>
+file which test the <tt>unionmap</tt> implementation in the file.
+<tt>dict_union.c</tt>. </p>
+
+<p> Background: a unionmap creates a union of tables. For example,
+the lookup table "<tt>unionmap:{inline:{foo=one},inline:{foo=two}}</tt>"
+will return ("<tt>one, two</tt>", DICT_STAT_SUCCESS) when queried
+with <tt>foo</tt>, and will return (NOTFOUND, DICT_STAT_SUCCESS)
+otherwise. </p>
+
+<p> First, we present the TEST_CASE structure with additional fields
+for inputs and expected results. </p>
+
+<blockquote>
+<pre>
+ 29 #define MAX_PROBE       5
+ 30 
+ 31 struct probe {
+ 32     const char *query;
+ 33     const char *want_value;
+ 34     int     want_error;
+ 35 };
+ 36 
+ 37 typedef struct PTEST_CASE {
+ 38     const char *testname;
+ 39     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+ 40     const char *type_name;
+ 41     const struct probe probes[MAX_PROBE];
+ 42 } PTEST_CASE;
+</pre>
+</blockquote>
+
+<p> In the PTEST_CASE structure above: </p>
+
+<ul>
+
+<li> <p> The <tt>testname</tt> and <tt>action</tt> fields are
+standard. We have seen these already in the simple example above.
+<p>
+
+<li> <p> The <tt>type_name</tt> field will contain the name of the table,
+for example <tt>unionmap:{static:one,inline:{foo=two}}</tt>. </p>
+
+<li> <p> The <tt>probes</tt> field contains a list of (query, expected
+result value, expected error code) that will be used to query the unionmap
+and to verify the result value and error code.
+</p>
+
+</ul>
+
+<p> Next we show the test data. Every test calls the same
+<tt>test_dict_union()</tt> function with a different <tt>unionmap</tt>
+configuration and with a list of queries with expected results. The
+implementation of that function follows after the test data. </p>
+
+<blockquote>
+<pre>
+ 78 static const PTEST_CASE ptestcases[] = {
+ 79     {
+ 80          /* testname */ "successful lookup: static map + inline map",
+ 81          /* action */ test_dict_union,
+ 82          /* type_name */ "unionmap:{static:one,inline:{foo=two}}",
+ 83          /* probes */ {
+ 84             {"foo", "one,two", DICT_STAT_SUCCESS},
+ 85             {"bar", "one", DICT_STAT_SUCCESS},
+ 86         },
+ 87     }, {
+ 88          /* testname */ "error propagation: static map + fail map",
+ 89          /* action */ test_dict_union,
+ 90          /* type_name */ "unionmap:{static:one,fail:fail}",
+ 91          /* probes */ {
+ 92             {"foo", 0, DICT_STAT_ERROR},
+ 93         },
+...
+102 };
+103 
+104 #include &lt;ptest_main.h&gt;
+</pre>
+</blockquote>
+
+<p> Finally, here is the <tt>test_dict_union()</tt> function that
+tests the <tt>unionmap</tt> implementation with a given configuration
+and test queries. </p>
+
+<blockquote>
+<pre>
+ 44 #define STR_OR_NULL(s)  ((s) ? (s) : "null")
+ 45 
+ 46 static void test_dict_union(PTEST_CTX *t, const struct PTEST_CASE *tp)
+ 47 {
+ 48     DICT   *dict;
+ 49     const struct probe *pp;
+ 50     const char *got_value;
+ 51     int     got_error;
+ 52 
+ 53     if ((dict = dict_open(tp->type_name, O_RDONLY, 0)) == 0)
+ 54         ptest_fatal(t, "dict_open(\"%s\", O_RDONLY, 0) failed: %m",
+ 55                     tp->type_name);
+ 56     for (pp = tp->probes; pp < tp->probes + MAX_PROBE && pp->query != 0; pp++) {
+ 57         got_value = dict_get(dict, pp->query);
+ 58         got_error = dict->error;
+ 59         if (got_value == 0 && pp->want_value == 0)
+ 60             continue;
+ 61         if (got_value == 0 || pp->want_value == 0) {
+ 62             ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want '%s'",
+ 63                         pp->query, STR_OR_NULL(got_value),
+ 64                         STR_OR_NULL(pp->want_value));
+ 65             break;
+ 66         }
+ 67         if (strcmp(got_value, pp->want_value) != 0) {
+ 68             ptest_error(t, "dict_get(dict, \"%s\"): got '%s', want '%s'",
+ 69                         pp->query, got_value, pp->want_value);
+ 70         }
+ 71         if (got_error != pp->want_error)
+ 72             ptest_error(t, "dict_get(dict,\"%s\") error: got %d, want %d",
+ 73                         pp->query, got_error, pp->want_error);
+ 74     }
+ 75     dict_free(dict);
+ 76 }
+</pre> 
+</blockquote>
+
+<p> A test run looks like this: </p>
+
+<blockquote>
+<pre>
+$ make test_dict_union
+...compiler output...
+LD_LIBRARY_PATH=/path/to/postfix-source/lib ./dict_union_test
+RUN  successful lookup: static map + inline map
+PASS successful lookup: static map + inline map
+RUN  error propagation: static map + fail map
+PASS error propagation: static map + fail map
+...
+dict_union_test: PASS: 3, SKIP: 0, FAIL: 0
+</pre>
+</blockquote>
+
+<h2> <a name="sub_tests"> Testing functions with subtests </a> </h2>
+
+<p> Sometimes it is not convenient to store test data in a PTEST_CASE
+structure. This can happen when converting an existing test into
+Ptest, or when the module under test contains multiple functions
+that need different kinds of test data. The solution is to create
+a <tt>_test.c</tt> file with the structure shown below. The example
+is based on code in <tt>map_search_test.c</tt> that was converted
+from an existing test into Ptest. </p>
+
+<ul>
+
+<li> <p> One PTEST_CASE structure definition without test data. </p>
+
+<pre>
+ 50 typedef struct PTEST_CASE {
+ 51     const char *testname;
+ 52     void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+ 53 } PTEST_CASE;
+</pre>
+
+<li> <p> One test function for each module function that needs to
+be tested, and one table with test cases for that module function.
+In this case there is only one module function (<tt>map_search()</tt>)
+that needs to be tested, so there is only one test function
+(<tt>test_map_search()</tt>). </p>
+
+<pre>
+ 67 #define MAX_WANT_LOG    5
+ 68 
+ 69 static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE *unused)
+ 70 {
+ 71     /* Test cases with inputs and expected outputs. */
+ 72     struct test {
+ 73         const char *map_spec;
+ 74         int     want_return;            /* 0=fail, 1=success */
+ 75         const char *want_log[MAX_WANT_LOG];
+ 76         const char *want_map_type_name; /* 0 or match */
+ 77         const char *exp_search_order;   /* 0 or match */
+ 78     };
+ 79     static struct test test_cases[] = {
+ 80         { /* 0 */ "type", 0, {
+ 81                 "malformed map specification: 'type'",
+ 82                 "expected maptype:mapname instead of 'type'",
+ 83         }, 0},
+...        // ...other test cases...
+111     };
+</pre>
+
+<li> <p> In a test function, iterate over its table with test cases,
+using <tt>PTEST_RUN()</tt> to run each test case in its own subtest.
+</p>
+
+<pre>
+129     for (tp = test_cases; tp->map_spec; tp++) {
+130         vstring_sprintf(test_label, "test %d", (int) (tp - test_cases));
+131         PTEST_RUN(t, STR(test_label), {
+132             for (cpp = tp->want_log; cpp < tp->want_log + MAX_WANT_LOG && *cpp; cpp++)
+133                 expect_ptest_log_event(t, *cpp);
+134             map_search_from_create = map_search_create(tp->map_spec);
+...            // ...verify that the result is as expected...
+...            // ...use ptest_return() or ptest_fatal() to exit from a test...
+173         });
+174     }
+...
+178 }
+</pre>
+
+<li> <p> Create a <tt>ptestcases[]</tt> table to call each test
+function once, and include the Ptest main program. </p>
+
+<pre>
+183 static const PTEST_CASE ptestcases[] = {
+184     "test_map_search", test_map_search,
+185 };
+186 
+187 #include &lt;ptest_main.h&gt;
+</pre>
+
+</ul>
+
+<p> See the file <tt>map_search_test.c</tt> for a complete example.
+</p>
+
+<p> This is what a test run looks like: </p>
+
+<blockquote>
+<pre>
+$ make test_map_search
+...compiler output...
+LD_LIBRARY_PATH=/path/to/postfix-source/lib  ./map_search_test
+RUN  test_map_search
+RUN  test_map_search/test 0
+PASS test_map_search/test 0
+....
+PASS test_map_search
+map_search_test: PASS: 13, SKIP: 0, FAIL: 0
+</pre>
+</blockquote>
+
+<p> This shows that the subtest name is appended to the parent test
+name, formatted as <i>parent-name/child-name</i>. </p>
+
+<h2> <a name="style"> Suggestions for writing tests </a> </h2>
+
+<p> Ptest is loosely inspired on Go test, especially its top-level
+test functions and its methods <tt>T.run()</tt>, <tt>T.error()</tt>
+and <tt>T.fatal()</tt>. </p>
+
+<p> Suggestions for test style may look familiar to Go programmers:
+</p>
+
+<ul>
+
+<li> <p> Use variables named <tt>got_xxx</tt> and <tt>want_xxx</tt>,
+and when a test result is unexpected, log the discrepancy as "got
+&lt;what you got&gt;, want &lt;what you want&gt;".  </p>
+
+<li> <p> Report discrepancies with <tt>ptest_error()</tt> if possible;
+use <tt>ptest_fatal()</tt> only when continuing the test would
+produce nonsensical results. </p>
+
+<li> <p> Where it makes sense use a table with testcases and use
+<tt>PTEST_RUN()</tt> to run each testcase in its own subtest. </p>
+
+</ul>
+
+<p> Other suggestions: </p>
+
+<ul>
+
+<li> <p> Consider running tests under a memory checker such as
+Valgrind. Use <tt>ptest_defer()</tt> to avoid memory leaks when a
+test may terminate early. </p>
+
+<li> <p> Always test non-error and error cases, to cover all code
+paths in the function under test. </p>
+
+</ul>
+
+<h2> <a name="api_reference"> Ptest API reference </a> </h2>
+
+<ul>
+
+<li> <p> <a href="#managing_errors">Managing test errors</a>
+
+<li> <p> <a href="#managing_logs">Managing log events</a>
+
+<li> <p> <a href="#managing_flows">Managing test execution </a>
+
+</ul>
+
+<h2> <a name="managing_errors"> Managing test errors</a> </h2>
+
+<p> As one might expect, Ptest has support to flag unexpected test
+results as errors. </p>
+
+<dl>
+
+<dt> <b> <a name="ptest_error"> <tt>void ptest_error(PTEST_CTX *t,
+const char *format, ...)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to report an unexpected test result,
+and to flag the test as failed without terminating the test. This
+call can be ignored with <tt>expect_ptest_error()</tt>. <br> <br> </dd>
+
+<dt> <b> <a name="ptest_fatal"> <tt>void ptest_fatal(PTEST_CTX *t,
+const char *format, ...)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to report an unexpected test result,
+to flag the test as failed, and to terminate the test. This call
+cannot be ignored with <tt>expect_ptest_error()</tt>. </dd>
+
+</dl>
+
+<p> For convenience, Ptest has can also report non-error information.
+</p>
+
+<dl>
+
+<dt> <b> <a name="ptest_info"> <tt>void ptest_info(PTEST_CTX *t,
+const char *format, ...)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to report a non-error condition
+without terminating the test. This call cannot be ignored with
+<tt>expect_ptest_error()</tt>. </dd>
+
+</dl>
+
+<p> Finally, Ptest has support to test <tt>ptest_error()</tt> itself,
+to verify that an intentional error is reported as expected. </p>
+
+<dl>
+
+<dt> <b> <a name="expect_ptest_error"> <tt>void
+expect_ptest_error(PTEST_CTX *t, const char *text)</tt> </a> </b>
+</dt>
+
+<dd> Called from inside a test to expect exactly one <tt>ptest_error()</tt>
+call with the specified text, and to ignore that <tt>ptest_error()</tt>
+call (i.e. don't flag the test as failed). To ignore multiple calls,
+call <tt>expect_ptest_error()</tt> multiple times. A test is flagged
+as failed when an expected error is not reported (and of course
+when an error is reported that is not expected with
+<tt>expect_ptest_error()</tt>). </dd>
+
+</dl>
+
+<h2> <a name="managing_logs"> Managing log events</a> </h2>
+
+<p> Ptest integrates with Postfix <tt>msg(3)</tt> logging. </p>
+
+<ul>
+
+<li> <p> Ptest changes the control flow of <tt>msg_fatal()</tt> and
+<tt>msg_panic()</tt>. When these functions are called during a test,
+Ptest flags a test as failed and terminates the test instead of the
+process. </p>
+
+<li> <p> Ptest silences the output from <tt>msg_info()</tt> and
+other <tt>msg(3)</tt> calls, and installs a log event listener tp
+monitor Postfix logging. </p>
+
+</ul>
+
+<p> Ptest provides the following API to manage log events: </p>
+
+<dl>
+
+<dt> <b> <a name="expect_ptest_log_event"> <tt>void
+expect_ptest_log_event(PTEST_CTX *t, const char *text)</tt> </a>
+</b> </dt>
+
+<dd> Called from inside a test to expect exactly one <tt>msg(3)</tt>
+call with the specified text. To expect multiple events, call
+<tt>expect_ptest_log_event()</tt> multiple times. A test is flagged
+as failed when expected text is not logged, or when text is logged
+that is not expected with <tt>expect_ptest_log_event()</tt>. </dd>
+
+</dl>
+
+<h2> <a name="managing_flows"> Managing test execution </a> </h2>
+
+<p> Ptest has a number of primitives that control test execution.
+</p>
+
+<dl>
+
+<dt> <b> <a name="PTEST_RUN"> <tt>void PTEST_RUN(PTEST_CTX *t, const
+char *test_name, { code in braces })</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to run the <tt>{ code in braces
+}</tt> in it own subtest environment. In the test progress report,
+the subtest name is appended to the parent test name, formatted as
+<i>parent-name/child-name</i>. <br> <br> NOTE: because <tt>PTEST_RUN()
+</tt> is a macro, the <tt>{ code in braces }</tt> must not contain
+a <tt>return</tt> statement; use <tt>ptest_return()</tt> instead.
+It is OK for <tt>{ code in braces }</tt> to call a function that
+uses <tt>return</tt>. <br> <br> </dd>
+
+<dt> <b> <a name="ptest_skip"> <tt>NORETURN ptest_skip(PTEST_CTX
+*t)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to flag a test as skipped, and to
+terminate the test without terminating the process. Use this to
+disable tests that are not applicable for a specific system type
+or build configuration. <br> <br> </dd>
+
+<dt> <b> <a name="ptest_return"> <tt>NORETURN ptest_return(PTEST_CTX
+*t)</tt> </a> </b> </dt>
+
+<dd> Called from inside a test to terminate the test without
+terminating the process. <br> <br> </dd>
+
+<dt> <b> <a name="ptest_defer"> <tt>void ptest_defer(PTEST_CTX *t,
+void (*defer_fn)(void *), void  *defer_ctx)</tt> </a> </b> </dt>
+
+<dd> Called once from inside a test, to call <tt>defer_fn(defer_ctx)</tt>
+after the test completes. This is typically used to eliminate a
+resource leak in tests that terminate the test early. <br> <br>
+NOTE: The deferred function is designed to run outside a test, and
+therefore it must not call Ptest functions. </dd>
+
+</dl>
+
+</body>
+
+</html>
+
index c3aaad7bd5449296418575d8b998c2c7aa6357fb..3e2025a7a14a9db724c76e0f70477ecb912a3b38 100644 (file)
@@ -280,6 +280,14 @@ configuration file in <code>/etc/postfix/sasl/</code>,
 cyrus_sasl_config_path</code> and/or the distribution-specific
 documentation to determine the expected location. </p> </li>
 
+<li> <p> Some Debian-based Postfix distributions patch Postfix to
+hardcode a non-default search path, making it impossible to set an
+alternate search path via the "cyrus_sasl_config_path" parameter.  This
+is likely to be the case when the distribution documents a
+Postfix-specific path (e.g. <code>/etc/postfix/sasl/</code>) that is
+different from the default value of "cyrus_sasl_config_path" (which
+then is likely to be empty). </p> </li>
+
 </ul>
 
 <blockquote>
index 189fb08dd18e78a469be782218883c440170d4f5..dd0a5494ad097e33d65506b664f69ddc06c32b5e 100644 (file)
@@ -118,6 +118,7 @@ server_address=10.3.2.1
 server_port=54321
 <b>Postfix version 3.8 and later:</b>
 compatibility_level=<i>major</i>.<i>minor</i>.<i>patch</i>
+mail_version=3.8.0
 [empty line]
 </pre>
 </blockquote>
@@ -220,6 +221,12 @@ compatibility_level=<i>major</i>.<i>minor</i>.<i>patch</i>
    <i>major</i>.<i>minor</i>.<i>patch</i> where <i>minor</i> and
    <i>patch</i> may be absent. </p>
 
+   <li> <p> The "mail_version" attribute corresponds to the
+   mail_version parameter value. It has the form
+   <i>major</i>.<i>minor</i>.<i>patch</i> for stable releases, and
+   <i>major</i>.<i>minor</i>-<i>yyyymmdd</i> for unstable releases.
+   </p>
+
 </ul>
 
 <p> The following is specific to SMTPD delegated policy requests:
index ed01ec0c96cb45b4d71daf096b6860d0b1a49948..d2d3f19b54de0bbb20432ec27d90fe584aa6162c 100644 (file)
 #      Mail is forwarded to \fIaddress\fR, which is compatible
 #      with the RFC 822 standard.
 # .IP \fI/file/name\fR
-#      Mail is appended to \fI/file/name\fR. See \fBlocal\fR(8)
-#      for details of delivery to file.
+#      Mail is appended to \fI/file/name\fR. For details on how a
+#      file is written see the sections "EXTERNAL FILE DELIVERY"
+#      and "DELIVERY RIGHTS" in the \fBlocal\fR(8) documentation.
 #      Delivery is not limited to regular files.  For example, to dispose
 #      of unwanted mail, deflect it to \fB/dev/null\fR.
 # .IP "|\fIcommand\fR"
-#      Mail is piped into \fIcommand\fR. Commands that contain special
-#      characters, such as whitespace, should be enclosed between double
-#      quotes. See \fBlocal\fR(8) for details of delivery to command.
+#      Mail is piped into \fIcommand\fR. Commands that contain
+#      special characters, such as whitespace, should be enclosed
+#      between double quotes. For details on how a command is
+#      executed see "EXTERNAL COMMAND DELIVERY" and "DELIVERY
+#      RIGHTS" in the \fBlocal\fR(8) documentation.
 # .sp
 #      When the command fails, a limited amount of command output is
 #      mailed back to the sender.  The file \fB/usr/include/sysexits.h\fR
index 76919f0ca0970d80c4f4352893cadbb1cfbe40a1..0f335eea92514e890d3f63822356ab28de1013dd 100644 (file)
@@ -14354,13 +14354,16 @@ The default time unit is s (seconds).  </p>
 
 %PARAM postscreen_dnsbl_sites
 
-<p>Optional list of DNS allow/denylist domains, filters and weight
+<p>Optional list of patterns with DNS allow/denylist domains, filters
+and weight
 factors. When the list is non-empty, the dnsblog(8) daemon will
-query these domains with the IP addresses of remote SMTP clients,
+query these domains with the reversed IP addresses of remote SMTP
+clients,
 and postscreen(8) will update an SMTP client's DNSBL score with
-each non-error reply. </p>
+each non-error reply as described below. </p>
 
-<p> Caution: when postscreen rejects mail, it replies with the DNSBL
+<p> Caution: when postscreen rejects mail, its SMTP response contains
+the DNSBL
 domain name. Use the postscreen_dnsbl_reply_map feature to hide
 "password" information in DNSBL domain names. </p>
 
@@ -14368,26 +14371,25 @@ domain name. Use the postscreen_dnsbl_reply_map feature to hide
 specified with postscreen_dnsbl_threshold, postscreen(8) can drop
 the connection with the remote SMTP client. </p>
 
-<p> Specify a list of domain=filter*weight entries, separated by
+<p> Specify a list of domain=filter*weight patterns, separated by
 comma or whitespace.  </p>
 
 <ul>
 
-<li> <p> When no "=filter" is specified, postscreen(8) will use any
-non-error DNSBL reply.  Otherwise, postscreen(8) uses only DNSBL
-replies that match the filter. The filter has the form d.d.d.d,
+<li> <p> When a pattern specifies no "=filter", postscreen(8) will
+use any non-error DNSBL query result.  Otherwise, postscreen(8)
+will use only DNSBL
+query results that match the filter. The filter has the form d.d.d.d,
 where each d is a number, or a pattern inside [] that contains one
 or more ";"-separated numbers or number..number ranges.  </p>
 
-<li> <p> When no "*weight" is specified, postscreen(8) increments
-the remote SMTP client's DNSBL score by 1.  Otherwise, the weight must be
-an integral number, and postscreen(8) adds the specified weight to
-the remote SMTP client's DNSBL score.  Specify a negative number for
-allowlisting.  </p>
+<li> <p> When a pattern specifies no "*weight", the weight of the
+pattern is 1.  Otherwise, the weight must be an integral number.
+Specify a negative number for allowlisting.  </p>
 
-<li> <p> When one postscreen_dnsbl_sites entry produces multiple
-DNSBL responses, postscreen(8) applies the weight at most once.
-</p>
+<li> <p> When a pattern matches one or more DNSBL query results,
+postscreen(8) adds that pattern's weight once to the remote SMTP
+client's DNSBL score. </p>
 
 </ul>
 
index a7e78243d531235f561bbb9f3a09531377b87991..854800873b9eb2f1d8e58f75f8a5146eb57baa18 100644 (file)
@@ -245,3 +245,7 @@ dt  dt b name value b Postfix ge 3 0 dt
  dt dt dd 4 Also log the hexadecimal and ASCII dump of complete
  parametername stress something something Other
  p Note on OpenBSD systems specify dev dev arandom when dev dev urandom
+ 90 type_name unionmap static one fail fail 
+ a structure as shown shown below The example is based on code in
+184 test_map_search test_map_search 
+ void ptest_defer PTEST_CTX t void defer_fn void void
index 550d998d058c5062890d6ede72235f402572be58..1289948b54b2785027e1bbc37a7b6d4701377790 100644 (file)
@@ -1838,3 +1838,10 @@ ptestcase
 ptestcases
 subtests
 case's
+HAPROXY
+SRVR
+Deserialize
+SNDBUF
+deserialize
+deserialized
+NORAMDOMIZE
index 616fb118e755ec2fdbd48c325bb31b6f2ef0ae14..9aeac184424a7fe7312d9100018e3077d698ac66 100644 (file)
@@ -349,3 +349,42 @@ J
 ng
 rsyslogd
 ptest
+DICT
+Gmock
+LD
+NORETURN
+NOTFOUND
+PTEST
+Pmock
+Ptest
+RDONLY
+STAT
+STR
+Valgrind
+addrinfo
+api
+const
+cpp
+eq
+exp
+fn
+myfree
+mymalloc
+pp
+ptestcases
+struct
+subtests
+testcase
+testcases
+testname
+tp
+vstring
+subtest
+templating
+typedef
+Gtest
+mymemdup
+myrealloc
+mystrdup
+hardcode
+pattern's
index a8c7ccd4c51f5ac05f743aca4bcf092970d931c5..02ed188569339864a784d625f57c57abfe83d203 100644 (file)
@@ -83,7 +83,7 @@ OBJS  = abounce.o anvil_clnt.o been_here.o bounce.o bounce_log.o \
 MAP_OBJ = dict_ldap.o dict_mysql.o dict_pgsql.o dict_sqlite.o
 TEST_OBJ = normalize_mailhost_addr_test.o smtp_reply_footer_test.o \
        login_sender_match_test.o map_search_test.o delivered_hdr_test.o \
-       config_known_tcp_ports_test.o hfrom_format_test.o
+       config_known_tcp_ports_test.o hfrom_format_test.o haproxy_srvr_test.o
 HDRS   = abounce.h anvil_clnt.h been_here.h bounce.h bounce_log.h \
        canon_addr.h cfg_parser.h cleanup_user.h clnt_stream.h config.h \
        conv_time.h db_common.h debug_peer.h debug_process.h defer.h \
@@ -133,12 +133,13 @@ TESTPROG= domain_list dot_lockfile mail_addr_crunch mail_addr_find \
        data_redirect addr_match_list safe_ultostr verify_sender_addr \
        mail_version mail_dict server_acl uxtext mail_parm_split \
        fold_addr smtp_reply_footer_test mail_addr_map \
-       normalize_mailhost_addr_test haproxy_srvr map_search_test \
+       normalize_mailhost_addr_test haproxy_srvr_test map_search_test \
        delivered_hdr_test login_sender_match_test \
        compat_level config_known_tcp_ports_test hfrom_format_test
 
 LIBS   = ../../lib/lib$(LIB_PREFIX)util$(LIB_SUFFIX)
-TEST_LIB= ../../lib/libtesting.a ../../lib/libptest.a
+PTEST_LIB= ../../lib/libptest.a
+PMOCK_LIB= ../../lib/libtesting.a
 LIB_DIR        = ../../lib
 INC_DIR        = ../../include
 PLUGIN_MAP_SO = $(LIB_PREFIX)ldap$(LIB_SUFFIX) $(LIB_PREFIX)mysql$(LIB_SUFFIX) \
@@ -390,7 +391,7 @@ tests: tok822_test mime_tests strip_addr_test tok822_limit_test \
        safe_ultostr_test mail_parm_split_test fold_addr_test \
        test_smtp_reply_footer off_cvt_test mail_addr_crunch_test \
        mail_addr_find_test mail_addr_map_test quote_822_local_test \
-       test_normalize_mailhost_addr haproxy_srvr_test test_map_search \
+       test_normalize_mailhost_addr test_haproxy_srvr test_map_search \
        delivered_hdr_test test_login_sender_match compat_level_test \
        test_config_known_tcp_ports test_hfrom_format
 
@@ -674,8 +675,8 @@ fold_addr_test: fold_addr fold_addr_test.in fold_addr_test.ref
        diff fold_addr_test.ref fold_addr_test.tmp
        rm -f fold_addr_test.tmp
 
-smtp_reply_footer_test: smtp_reply_footer_test.o $(TEST_LIB) $(LIB) $(LIBS)
-        $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
+smtp_reply_footer_test: smtp_reply_footer_test.o $(PTEST_LIB) $(LIB) $(LIBS)
+        $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
 
 test_smtp_reply_footer: smtp_reply_footer_test
        $(SHLIB_ENV) $(VALGRIND) ./smtp_reply_footer_test
@@ -707,32 +708,34 @@ quote_822_local_test: update quote_822_local quote_822_local.in quote_822_local.
        rm -f quote_822_local.tmp
 
 normalize_mailhost_addr_test: normalize_mailhost_addr_test.o \
-       $(TEST_LIB) $(LIB) $(LIBS)
-       $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
+       $(PTEST_LIB) $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
 
 test_normalize_mailhost_addr: update normalize_mailhost_addr_test
        $(SHLIB_ENV) $(VALGRIND) ./normalize_mailhost_addr_test
 
-haproxy_srvr_test: update haproxy_srvr
-       -$(SHLIB_ENV) $(VALGRIND) ./haproxy_srvr >haproxy_srvr.tmp 2>&1
-       diff /dev/null haproxy_srvr.tmp
-       rm -f haproxy_srvr.tmp
+haproxy_srvr_test: haproxy_srvr_test.o \
+       $(PTEST_LIB) $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
 
-map_search_test: map_search_test.o $(TEST_LIB) $(LIB) $(LIBS)
-       $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
+test_haproxy_srvr: update haproxy_srvr_test
+       $(SHLIB_ENV) $(VALGRIND) ./haproxy_srvr_test
+
+map_search_test: map_search_test.o $(PTEST_LIB) $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
 
 test_map_search: update map_search_test 
        $(SHLIB_ENV) $(VALGRIND) ./map_search_test
 
-delivered_hdr_test: delivered_hdr_test.o $(TEST_LIB) $(LIB) $(LIBS)
-       $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
+delivered_hdr_test: delivered_hdr_test.o $(PTEST_LIB) $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
 
 test_delivered_hdr: update delivered_hdr_test 
        $(SHLIB_ENV) $(VALGRIND) ./delivered_hdr_test
 
 login_sender_match_test: login_sender_match_test.o \
-       $(TEST_LIB) $(LIB) $(LIBS)
-       $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
+       $(PTEST_LIB) $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
 
 test_login_sender_match: update login_sender_match_test
        $(SHLIB_ENV) $(VALGRIND) ./login_sender_match_test
@@ -751,14 +754,14 @@ compat_level_convert_test: update compat_level compat_level_convert.in \
        diff compat_level_convert.ref compat_level_convert.tmp
        rm -f compat_level_convert.tmp
 
-config_known_tcp_ports_test: config_known_tcp_ports_test.o $(TEST_LIB) $(LIB) $(LIBS)
-       $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
+config_known_tcp_ports_test: config_known_tcp_ports_test.o $(PTEST_LIB) $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
 
 test_config_known_tcp_ports: update config_known_tcp_ports_test
        $(SHLIB_ENV) $(VALGRIND) ./config_known_tcp_ports_test
 
-hfrom_format_test: hfrom_format_test.o $(TEST_LIB) $(LIB) $(LIBS)
-       $(CC) $(CFLAGS) -o $@ $@.o $(TEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
+hfrom_format_test: hfrom_format_test.o $(PTEST_LIB) $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o $(PTEST_LIB) $(LIB) $(LIBS) $(SYSLIBS)
 
 test_hfrom_format: update hfrom_format_test
        $(SHLIB_ENV) $(VALGRIND) ./hfrom_format_test
@@ -1197,26 +1200,8 @@ delivered_hdr_test.o: fold_addr.h
 delivered_hdr_test.o: mail_params.h
 delivered_hdr_test.o: rec_type.h
 delivered_hdr_test.o: record.h
-dict_ldap.o: ../../include/argv.h
-dict_ldap.o: ../../include/binhash.h
-dict_ldap.o: ../../include/check_arg.h
-dict_ldap.o: ../../include/dict.h
-dict_ldap.o: ../../include/match_list.h
-dict_ldap.o: ../../include/msg.h
-dict_ldap.o: ../../include/myflock.h
-dict_ldap.o: ../../include/mymalloc.h
-dict_ldap.o: ../../include/name_code.h
-dict_ldap.o: ../../include/stringops.h
 dict_ldap.o: ../../include/sys_defs.h
-dict_ldap.o: ../../include/vbuf.h
-dict_ldap.o: ../../include/vstream.h
-dict_ldap.o: ../../include/vstring.h
-dict_ldap.o: cfg_parser.h
-dict_ldap.o: db_common.h
 dict_ldap.o: dict_ldap.c
-dict_ldap.o: dict_ldap.h
-dict_ldap.o: mail_conf.h
-dict_ldap.o: string_list.h
 dict_memcache.o: ../../include/argv.h
 dict_memcache.o: ../../include/auto_clnt.h
 dict_memcache.o: ../../include/check_arg.h
@@ -1236,47 +1221,10 @@ dict_memcache.o: dict_memcache.c
 dict_memcache.o: dict_memcache.h
 dict_memcache.o: memcache_proto.h
 dict_memcache.o: string_list.h
-dict_mysql.o: ../../include/argv.h
-dict_mysql.o: ../../include/check_arg.h
-dict_mysql.o: ../../include/dict.h
-dict_mysql.o: ../../include/events.h
-dict_mysql.o: ../../include/find_inet.h
-dict_mysql.o: ../../include/match_list.h
-dict_mysql.o: ../../include/msg.h
-dict_mysql.o: ../../include/myflock.h
-dict_mysql.o: ../../include/mymalloc.h
-dict_mysql.o: ../../include/myrand.h
-dict_mysql.o: ../../include/split_at.h
-dict_mysql.o: ../../include/stringops.h
 dict_mysql.o: ../../include/sys_defs.h
-dict_mysql.o: ../../include/vbuf.h
-dict_mysql.o: ../../include/vstream.h
-dict_mysql.o: ../../include/vstring.h
-dict_mysql.o: cfg_parser.h
-dict_mysql.o: db_common.h
 dict_mysql.o: dict_mysql.c
-dict_mysql.o: dict_mysql.h
-dict_mysql.o: string_list.h
-dict_pgsql.o: ../../include/argv.h
-dict_pgsql.o: ../../include/check_arg.h
-dict_pgsql.o: ../../include/dict.h
-dict_pgsql.o: ../../include/events.h
-dict_pgsql.o: ../../include/match_list.h
-dict_pgsql.o: ../../include/msg.h
-dict_pgsql.o: ../../include/myflock.h
-dict_pgsql.o: ../../include/mymalloc.h
-dict_pgsql.o: ../../include/myrand.h
-dict_pgsql.o: ../../include/split_at.h
-dict_pgsql.o: ../../include/stringops.h
 dict_pgsql.o: ../../include/sys_defs.h
-dict_pgsql.o: ../../include/vbuf.h
-dict_pgsql.o: ../../include/vstream.h
-dict_pgsql.o: ../../include/vstring.h
-dict_pgsql.o: cfg_parser.h
-dict_pgsql.o: db_common.h
 dict_pgsql.o: dict_pgsql.c
-dict_pgsql.o: dict_pgsql.h
-dict_pgsql.o: string_list.h
 dict_proxy.o: ../../include/argv.h
 dict_proxy.o: ../../include/attr.h
 dict_proxy.o: ../../include/check_arg.h
@@ -1298,23 +1246,8 @@ dict_proxy.o: dict_proxy.c
 dict_proxy.o: dict_proxy.h
 dict_proxy.o: mail_params.h
 dict_proxy.o: mail_proto.h
-dict_sqlite.o: ../../include/argv.h
-dict_sqlite.o: ../../include/check_arg.h
-dict_sqlite.o: ../../include/dict.h
-dict_sqlite.o: ../../include/match_list.h
-dict_sqlite.o: ../../include/msg.h
-dict_sqlite.o: ../../include/myflock.h
-dict_sqlite.o: ../../include/mymalloc.h
-dict_sqlite.o: ../../include/stringops.h
 dict_sqlite.o: ../../include/sys_defs.h
-dict_sqlite.o: ../../include/vbuf.h
-dict_sqlite.o: ../../include/vstream.h
-dict_sqlite.o: ../../include/vstring.h
-dict_sqlite.o: cfg_parser.h
-dict_sqlite.o: db_common.h
 dict_sqlite.o: dict_sqlite.c
-dict_sqlite.o: dict_sqlite.h
-dict_sqlite.o: string_list.h
 domain_list.o: ../../include/argv.h
 domain_list.o: ../../include/check_arg.h
 domain_list.o: ../../include/match_list.h
@@ -1506,6 +1439,24 @@ haproxy_srvr.o: ../../include/vstring.h
 haproxy_srvr.o: ../../include/wrap_netdb.h
 haproxy_srvr.o: haproxy_srvr.c
 haproxy_srvr.o: haproxy_srvr.h
+haproxy_srvr_test.o: ../../include/argv.h
+haproxy_srvr_test.o: ../../include/check_arg.h
+haproxy_srvr_test.o: ../../include/msg.h
+haproxy_srvr_test.o: ../../include/msg_output.h
+haproxy_srvr_test.o: ../../include/msg_vstream.h
+haproxy_srvr_test.o: ../../include/myaddrinfo.h
+haproxy_srvr_test.o: ../../include/pmock_expect.h
+haproxy_srvr_test.o: ../../include/ptest.h
+haproxy_srvr_test.o: ../../include/ptest_main.h
+haproxy_srvr_test.o: ../../include/sock_addr.h
+haproxy_srvr_test.o: ../../include/stringops.h
+haproxy_srvr_test.o: ../../include/sys_defs.h
+haproxy_srvr_test.o: ../../include/vbuf.h
+haproxy_srvr_test.o: ../../include/vstream.h
+haproxy_srvr_test.o: ../../include/vstring.h
+haproxy_srvr_test.o: ../../include/wrap_netdb.h
+haproxy_srvr_test.o: haproxy_srvr.h
+haproxy_srvr_test.o: haproxy_srvr_test.c
 header_body_checks.o: ../../include/argv.h
 header_body_checks.o: ../../include/check_arg.h
 header_body_checks.o: ../../include/dict.h
index 63147c1c5f863389fa6610adb0ad8a0a619f6861..e349128c30c2681b5aae835c47c6b5eb88da20b3 100644 (file)
@@ -85,6 +85,7 @@
 
 /* Global library. */
 
+#define HAPROXY_SRVR_INTERNAL
 #include <haproxy_srvr.h>
 
 /* Application-specific. */
   */
 #define HAPROXY_HEADER_MAX_LEN         536
 
- /*
-  * Begin protocol v2 definitions from haproxy/include/types/connection.h.
-  */
-#define PP2_SIGNATURE          "\r\n\r\n\0\r\nQUIT\n"
-#define PP2_SIGNATURE_LEN      12
-#define PP2_HEADER_LEN         16
-
-/* ver_cmd byte */
-#define PP2_CMD_LOCAL          0x00
-#define PP2_CMD_PROXY          0x01
-#define PP2_CMD_MASK           0x0F
-
-#define PP2_VERSION            0x20
-#define PP2_VERSION_MASK       0xF0
-
-/* fam byte */
-#define PP2_TRANS_UNSPEC       0x00
-#define PP2_TRANS_STREAM       0x01
-#define PP2_TRANS_DGRAM                0x02
-#define PP2_TRANS_MASK         0x0F
-
-#define PP2_FAM_UNSPEC         0x00
-#define PP2_FAM_INET           0x10
-#define PP2_FAM_INET6          0x20
-#define PP2_FAM_UNIX           0x30
-#define PP2_FAM_MASK           0xF0
-
-/* len field (2 bytes) */
-#define PP2_ADDR_LEN_UNSPEC    (0)
-#define PP2_ADDR_LEN_INET      (4 + 4 + 2 + 2)
-#define PP2_ADDR_LEN_INET6     (16 + 16 + 2 + 2)
-#define PP2_ADDR_LEN_UNIX      (108 + 108)
-
-#define PP2_HDR_LEN_UNSPEC     (PP2_HEADER_LEN + PP2_ADDR_LEN_UNSPEC)
-#define PP2_HDR_LEN_INET       (PP2_HEADER_LEN + PP2_ADDR_LEN_INET)
-#define PP2_HDR_LEN_INET6      (PP2_HEADER_LEN + PP2_ADDR_LEN_INET6)
-#define PP2_HDR_LEN_UNIX       (PP2_HEADER_LEN + PP2_ADDR_LEN_UNIX)
-
-struct proxy_hdr_v2 {
-    uint8_t sig[PP2_SIGNATURE_LEN];    /* PP2_SIGNATURE */
-    uint8_t ver_cmd;                   /* protocol version | command */
-    uint8_t fam;                       /* protocol family and transport */
-    uint16_t len;                      /* length of remainder */
-    union {
-       struct {                        /* for TCP/UDP over IPv4, len = 12 */
-           uint32_t src_addr;
-           uint32_t dst_addr;
-           uint16_t src_port;
-           uint16_t dst_port;
-       }       ip4;
-       struct {                        /* for TCP/UDP over IPv6, len = 36 */
-           uint8_t src_addr[16];
-           uint8_t dst_addr[16];
-           uint16_t src_port;
-           uint16_t dst_port;
-       }       ip6;
-       struct {                        /* for AF_UNIX sockets, len = 216 */
-           uint8_t src_addr[108];
-           uint8_t dst_addr[108];
-       }       unx;
-    }       addr;
-};
-
- /*
-  * End protocol v2 definitions from haproxy/include/types/connection.h.
-  */
-
 static const INET_PROTO_INFO *proto_info;
 
 #define STR_OR_NULL(str) ((str) ? (str) : "(null)")
@@ -535,357 +469,3 @@ int     haproxy_srvr_receive(int fd, int *non_proxy,
     }
     return (0);
 }
-
- /*
-  * Test program.
-  */
-#ifdef TEST
-
- /*
-  * Test cases with inputs and expected outputs. A request may contain
-  * trailing garbage, and it may be too short. A v1 request may also contain
-  * malformed address or port information.
-  */
-typedef struct TEST_CASE {
-    const char *haproxy_request;       /* v1 or v2 request including thrash */
-    ssize_t haproxy_req_len;           /* request length including thrash */
-    ssize_t exp_req_len;               /* parsed request length */
-    int     exp_non_proxy;             /* request is not proxied */
-    const char *exp_return;            /* expected error string */
-    const char *exp_client_addr;       /* expected client address string */
-    const char *exp_server_addr;       /* expected client port string */
-    const char *exp_client_port;       /* expected client address string */
-    const char *exp_server_port;       /* expected server port string */
-} TEST_CASE;
-static TEST_CASE v1_test_cases[] = {
-    /* IPv6. */
-    {"PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1 123 321\n", 0, 0, 0, 0, "fc::1:2:3:4", "fc::4:3:2:1", "123", "321"},
-    {"PROXY TCP6 FC:00:00:00:1:2:3:4 FC:00:00:00:4:3:2:1 123 321\n", 0, 0, 0, 0, "fc::1:2:3:4", "fc::4:3:2:1", "123", "321"},
-    {"PROXY TCP6 1.2.3.4 4.3.2.1 123 321\n", 0, 0, 0, "bad or missing client address"},
-    {"PROXY TCP6 fc:00:00:00:1:2:3:4 4.3.2.1 123 321\n", 0, 0, 0, "bad or missing server address"},
-    /* IPv4 in IPv6. */
-    {"PROXY TCP6 ::ffff:1.2.3.4 ::ffff:4.3.2.1 123 321\n", 0, 0, 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"},
-    {"PROXY TCP6 ::FFFF:1.2.3.4 ::FFFF:4.3.2.1 123 321\n", 0, 0, 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"},
-    {"PROXY TCP4 ::ffff:1.2.3.4 ::ffff:4.3.2.1 123 321\n", 0, 0, 0, "bad or missing client address"},
-    {"PROXY TCP4 1.2.3.4 ::ffff:4.3.2.1 123 321\n", 0, 0, 0, "bad or missing server address"},
-    /* IPv4. */
-    {"PROXY TCP4 1.2.3.4 4.3.2.1 123 321\n", 0, 0, 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"},
-    {"PROXY TCP4 01.02.03.04 04.03.02.01 123 321\n", 0, 0, 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"},
-    {"PROXY TCP4 1.2.3.4 4.3.2.1 123456 321\n", 0, 0, 0, "bad or missing client port"},
-    {"PROXY TCP4 1.2.3.4 4.3.2.1 123 654321\n", 0, 0, 0, "bad or missing server port"},
-    {"PROXY TCP4 1.2.3.4 4.3.2.1 0123 321\n", 0, 0, 0, "bad or missing client port"},
-    {"PROXY TCP4 1.2.3.4 4.3.2.1 123 0321\n", 0, 0, 0, "bad or missing server port"},
-    /* Missing fields. */
-    {"PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1 123\n", 0, 0, 0, "bad or missing server port"},
-    {"PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1\n", 0, 0, 0, "bad or missing client port"},
-    {"PROXY TCP6 fc:00:00:00:1:2:3:4\n", 0, 0, 0, "bad or missing server address"},
-    {"PROXY TCP6\n", 0, 0, 0, "bad or missing client address"},
-    {"PROXY TCP4 1.2.3.4 4.3.2.1 123\n", 0, 0, 0, "bad or missing server port"},
-    {"PROXY TCP4 1.2.3.4 4.3.2.1\n", 0, 0, 0, "bad or missing client port"},
-    {"PROXY TCP4 1.2.3.4\n", 0, 0, 0, "bad or missing server address"},
-    {"PROXY TCP4\n", 0, 0, 0, "bad or missing client address"},
-    /* Other. */
-    {"PROXY BLAH\n", 0, 0, 0, "bad or missing protocol type"},
-    {"PROXY\n", 0, 0, 0, "short protocol header"},
-    {"BLAH\n", 0, 0, 0, "short protocol header"},
-    {"\n", 0, 0, 0, "short protocol header"},
-    {"", 0, 0, 0, "short protocol header"},
-    0,
-};
-
-static struct proxy_hdr_v2 v2_local_request = {
-    PP2_SIGNATURE, PP2_VERSION | PP2_CMD_LOCAL,
-};
-static TEST_CASE v2_non_proxy_test = {
-    (char *) &v2_local_request, PP2_HEADER_LEN, PP2_HEADER_LEN, 1,
-};
-
-#define STR(x) vstring_str(x)
-#define LEN(x) VSTRING_LEN(x)
-
-/* evaluate_test_case - evaluate one test case */
-
-static int evaluate_test_case(const char *test_label,
-                                     const TEST_CASE *test_case)
-{
-    /* Actual results. */
-    const char *act_return;
-    ssize_t act_req_len;
-    int     act_non_proxy;
-    MAI_HOSTADDR_STR act_smtp_client_addr;
-    MAI_HOSTADDR_STR act_smtp_server_addr;
-    MAI_SERVPORT_STR act_smtp_client_port;
-    MAI_SERVPORT_STR act_smtp_server_port;
-    int     test_failed;
-
-    if (msg_verbose)
-       msg_info("test case=%s exp_client_addr=%s exp_server_addr=%s "
-                "exp_client_port=%s exp_server_port=%s",
-                test_label, STR_OR_NULL(test_case->exp_client_addr),
-                STR_OR_NULL(test_case->exp_server_addr),
-                STR_OR_NULL(test_case->exp_client_port),
-                STR_OR_NULL(test_case->exp_server_port));
-
-    /*
-     * Start the test.
-     */
-    test_failed = 0;
-    act_req_len = test_case->haproxy_req_len;
-    act_return =
-       haproxy_srvr_parse(test_case->haproxy_request, &act_req_len,
-                          &act_non_proxy,
-                          &act_smtp_client_addr, &act_smtp_client_port,
-                          &act_smtp_server_addr, &act_smtp_server_port);
-    if (act_return != test_case->exp_return) {
-       msg_warn("test case %s return expected=%s actual=%s",
-                test_label, STR_OR_NULL(test_case->exp_return),
-                STR_OR_NULL(act_return));
-       test_failed = 1;
-       return (test_failed);
-    }
-    if (act_req_len != test_case->exp_req_len) {
-       msg_warn("test case %s str_len expected=%ld actual=%ld",
-                test_label,
-                (long) test_case->exp_req_len, (long) act_req_len);
-       test_failed = 1;
-       return (test_failed);
-    }
-    if (act_non_proxy != test_case->exp_non_proxy) {
-       msg_warn("test case %s non_proxy expected=%d actual=%d",
-                test_label,
-                test_case->exp_non_proxy, act_non_proxy);
-       test_failed = 1;
-       return (test_failed);
-    }
-    if (test_case->exp_non_proxy || test_case->exp_return != 0)
-       /* No expected address/port results. */
-       return (test_failed);
-
-    /*
-     * Compare address/port results against expected results.
-     */
-    if (strcmp(test_case->exp_client_addr, act_smtp_client_addr.buf)) {
-       msg_warn("test case %s client_addr  expected=%s actual=%s",
-                test_label,
-                test_case->exp_client_addr, act_smtp_client_addr.buf);
-       test_failed = 1;
-    }
-    if (strcmp(test_case->exp_server_addr, act_smtp_server_addr.buf)) {
-       msg_warn("test case %s server_addr  expected=%s actual=%s",
-                test_label,
-                test_case->exp_server_addr, act_smtp_server_addr.buf);
-       test_failed = 1;
-    }
-    if (strcmp(test_case->exp_client_port, act_smtp_client_port.buf)) {
-       msg_warn("test case %s client_port  expected=%s actual=%s",
-                test_label,
-                test_case->exp_client_port, act_smtp_client_port.buf);
-       test_failed = 1;
-    }
-    if (strcmp(test_case->exp_server_port, act_smtp_server_port.buf)) {
-       msg_warn("test case %s server_port  expected=%s actual=%s",
-                test_label,
-                test_case->exp_server_port, act_smtp_server_port.buf);
-       test_failed = 1;
-    }
-    return (test_failed);
-}
-
-/* convert_v1_proxy_req_to_v2 - convert well-formed v1 proxy request to v2 */
-
-static void convert_v1_proxy_req_to_v2(VSTRING *buf, const char *req,
-                                              ssize_t req_len)
-{
-    const char myname[] = "convert_v1_proxy_req_to_v2";
-    const char *err;
-    int     non_proxy;
-    MAI_HOSTADDR_STR smtp_client_addr;
-    MAI_SERVPORT_STR smtp_client_port;
-    MAI_HOSTADDR_STR smtp_server_addr;
-    MAI_SERVPORT_STR smtp_server_port;
-    struct proxy_hdr_v2 *hdr_v2;
-    struct addrinfo *src_res;
-    struct addrinfo *dst_res;
-
-    /*
-     * Allocate buffer space for the largest possible protocol header, so we
-     * don't have to worry about hidden realloc() calls.
-     */
-    VSTRING_RESET(buf);
-    VSTRING_SPACE(buf, sizeof(struct proxy_hdr_v2));
-    hdr_v2 = (struct proxy_hdr_v2 *) STR(buf);
-
-    /*
-     * Fill in the header,
-     */
-    memcpy(hdr_v2->sig, PP2_SIGNATURE, PP2_SIGNATURE_LEN);
-    hdr_v2->ver_cmd = PP2_VERSION | PP2_CMD_PROXY;
-    if ((err = haproxy_srvr_parse(req, &req_len, &non_proxy, &smtp_client_addr,
-                                 &smtp_client_port, &smtp_server_addr,
-                                 &smtp_server_port)) != 0 || non_proxy)
-       msg_fatal("%s: malformed or non-proxy request: %s",
-                 myname, req);
-
-    if (hostaddr_to_sockaddr(smtp_client_addr.buf, smtp_client_port.buf, 0,
-                            &src_res) != 0)
-       msg_fatal("%s: unable to convert source address %s port %s",
-                 myname, smtp_client_addr.buf, smtp_client_port.buf);
-    if (hostaddr_to_sockaddr(smtp_server_addr.buf, smtp_server_port.buf, 0,
-                            &dst_res) != 0)
-       msg_fatal("%s: unable to convert destination address %s port %s",
-                 myname, smtp_server_addr.buf, smtp_server_port.buf);
-    if (src_res->ai_family != dst_res->ai_family)
-       msg_fatal("%s: mixed source/destination address families", myname);
-#ifdef AF_INET6
-    if (src_res->ai_family == PF_INET6) {
-       hdr_v2->fam = PP2_FAM_INET6 | PP2_TRANS_STREAM;
-       hdr_v2->len = htons(PP2_ADDR_LEN_INET6);
-       memcpy(hdr_v2->addr.ip6.src_addr,
-              &SOCK_ADDR_IN6_ADDR(src_res->ai_addr),
-              sizeof(hdr_v2->addr.ip6.src_addr));
-       hdr_v2->addr.ip6.src_port = SOCK_ADDR_IN6_PORT(src_res->ai_addr);
-       memcpy(hdr_v2->addr.ip6.dst_addr,
-              &SOCK_ADDR_IN6_ADDR(dst_res->ai_addr),
-              sizeof(hdr_v2->addr.ip6.dst_addr));
-       hdr_v2->addr.ip6.dst_port = SOCK_ADDR_IN6_PORT(dst_res->ai_addr);
-    } else
-#endif
-    if (src_res->ai_family == PF_INET) {
-       hdr_v2->fam = PP2_FAM_INET | PP2_TRANS_STREAM;
-       hdr_v2->len = htons(PP2_ADDR_LEN_INET);
-       hdr_v2->addr.ip4.src_addr = SOCK_ADDR_IN_ADDR(src_res->ai_addr).s_addr;
-       hdr_v2->addr.ip4.src_port = SOCK_ADDR_IN_PORT(src_res->ai_addr);
-       hdr_v2->addr.ip4.dst_addr = SOCK_ADDR_IN_ADDR(dst_res->ai_addr).s_addr;
-       hdr_v2->addr.ip4.dst_port = SOCK_ADDR_IN_PORT(dst_res->ai_addr);
-    } else {
-       msg_panic("unknown address family 0x%x", src_res->ai_family);
-    }
-    vstring_set_payload_size(buf, PP2_SIGNATURE_LEN + ntohs(hdr_v2->len));
-    freeaddrinfo(src_res);
-    freeaddrinfo(dst_res);
-}
-
-int     main(int argc, char **argv)
-{
-    VSTRING *test_label;
-    TEST_CASE *v1_test_case;
-    TEST_CASE v2_test_case;
-    TEST_CASE mutated_test_case;
-    VSTRING *v2_request_buf;
-    VSTRING *mutated_request_buf;
-
-    /* Findings. */
-    int     tests_failed = 0;
-    int     test_failed;
-
-    test_label = vstring_alloc(100);
-    v2_request_buf = vstring_alloc(100);
-    mutated_request_buf = vstring_alloc(100);
-
-    for (tests_failed = 0, v1_test_case = v1_test_cases;
-        v1_test_case->haproxy_request != 0;
-        tests_failed += test_failed, v1_test_case++) {
-
-       /*
-        * Fill in missing string length info in v1 test data.
-        */
-       if (v1_test_case->haproxy_req_len == 0)
-           v1_test_case->haproxy_req_len =
-               strlen(v1_test_case->haproxy_request);
-       if (v1_test_case->exp_req_len == 0)
-           v1_test_case->exp_req_len = v1_test_case->haproxy_req_len;
-
-       /*
-        * Evaluate each v1 test case.
-        */
-       vstring_sprintf(test_label, "%d", (int) (v1_test_case - v1_test_cases));
-       test_failed = evaluate_test_case(STR(test_label), v1_test_case);
-
-       /*
-        * If the v1 test input is malformed, skip the mutation tests.
-        */
-       if (v1_test_case->exp_return != 0)
-           continue;
-
-       /*
-        * Mutation test: a well-formed v1 test case should still pass after
-        * appending a byte, and should return the actual parsed header
-        * length. The test uses the implicit VSTRING null safety byte.
-        */
-       vstring_sprintf(test_label, "%d (one byte appended)",
-                       (int) (v1_test_case - v1_test_cases));
-       mutated_test_case = *v1_test_case;
-       mutated_test_case.haproxy_req_len += 1;
-       /* reuse v1_test_case->exp_req_len */
-       test_failed += evaluate_test_case(STR(test_label), &mutated_test_case);
-
-       /*
-        * Mutation test: a well-formed v1 test case should fail after
-        * stripping the terminator.
-        */
-       vstring_sprintf(test_label, "%d (last byte stripped)",
-                       (int) (v1_test_case - v1_test_cases));
-       mutated_test_case = *v1_test_case;
-       mutated_test_case.exp_return = "missing protocol header terminator";
-       mutated_test_case.haproxy_req_len -= 1;
-       mutated_test_case.exp_req_len = mutated_test_case.haproxy_req_len;
-       test_failed += evaluate_test_case(STR(test_label), &mutated_test_case);
-
-       /*
-        * A 'well-formed' v1 test case should pass after conversion to v2.
-        */
-       vstring_sprintf(test_label, "%d (converted to v2)",
-                       (int) (v1_test_case - v1_test_cases));
-       v2_test_case = *v1_test_case;
-       convert_v1_proxy_req_to_v2(v2_request_buf,
-                                  v1_test_case->haproxy_request,
-                                  v1_test_case->haproxy_req_len);
-       v2_test_case.haproxy_request = STR(v2_request_buf);
-       v2_test_case.haproxy_req_len = PP2_HEADER_LEN
-           + ntohs(((struct proxy_hdr_v2 *) STR(v2_request_buf))->len);
-       v2_test_case.exp_req_len = v2_test_case.haproxy_req_len;
-       test_failed += evaluate_test_case(STR(test_label), &v2_test_case);
-
-       /*
-        * Mutation test: a well-formed v2 test case should still pass after
-        * appending a byte, and should return the actual parsed header
-        * length. The test uses the implicit VSTRING null safety byte.
-        */
-       vstring_sprintf(test_label, "%d (converted to v2, one byte appended)",
-                       (int) (v1_test_case - v1_test_cases));
-       mutated_test_case = v2_test_case;
-       mutated_test_case.haproxy_req_len += 1;
-       /* reuse v2_test_case->exp_req_len */
-       test_failed += evaluate_test_case(STR(test_label), &mutated_test_case);
-
-       /*
-        * Mutation test: a well-formed v2 test case should fail after
-        * stripping one byte
-        */
-       vstring_sprintf(test_label, "%d (converted to v2, last byte stripped)",
-                       (int) (v1_test_case - v1_test_cases));
-       mutated_test_case = v2_test_case;
-       mutated_test_case.haproxy_req_len -= 1;
-       mutated_test_case.exp_req_len = mutated_test_case.haproxy_req_len;
-       mutated_test_case.exp_return = "short version 2 protocol header";
-       test_failed += evaluate_test_case(STR(test_label), &mutated_test_case);
-    }
-
-    /*
-     * Additional V2-only tests.
-     */
-    test_failed +=
-       evaluate_test_case("v2 non-proxy request", &v2_non_proxy_test);
-
-    /*
-     * Clean up.
-     */
-    vstring_free(v2_request_buf);
-    vstring_free(mutated_request_buf);
-    vstring_free(test_label);
-    if (tests_failed)
-       msg_info("tests failed: %d", tests_failed);
-    exit(tests_failed != 0);
-}
-
-#endif
index 4a801f1d82bddf0855721be5b769006819fb9625..d2fe60a559ed340f8bbe0b7fedb0cf7f8b6b8348 100644 (file)
@@ -33,6 +33,79 @@ extern int haproxy_srvr_receive(int, int *,
 #define DONT_GRIPE     0
 #endif
 
+ /*
+  * Binary V2 protocol structure, also needed to create test data.
+  */
+#ifdef HAPROXY_SRVR_INTERNAL
+
+ /*
+  * Begin protocol v2 definitions from haproxy/include/types/connection.h.
+  */
+#define PP2_SIGNATURE          "\r\n\r\n\0\r\nQUIT\n"
+#define PP2_SIGNATURE_LEN      12
+#define PP2_HEADER_LEN         16
+
+/* ver_cmd byte */
+#define PP2_CMD_LOCAL          0x00
+#define PP2_CMD_PROXY          0x01
+#define PP2_CMD_MASK           0x0F
+
+#define PP2_VERSION            0x20
+#define PP2_VERSION_MASK       0xF0
+
+/* fam byte */
+#define PP2_TRANS_UNSPEC       0x00
+#define PP2_TRANS_STREAM       0x01
+#define PP2_TRANS_DGRAM                0x02
+#define PP2_TRANS_MASK         0x0F
+
+#define PP2_FAM_UNSPEC         0x00
+#define PP2_FAM_INET           0x10
+#define PP2_FAM_INET6          0x20
+#define PP2_FAM_UNIX           0x30
+#define PP2_FAM_MASK           0xF0
+
+/* len field (2 bytes) */
+#define PP2_ADDR_LEN_UNSPEC    (0)
+#define PP2_ADDR_LEN_INET      (4 + 4 + 2 + 2)
+#define PP2_ADDR_LEN_INET6     (16 + 16 + 2 + 2)
+#define PP2_ADDR_LEN_UNIX      (108 + 108)
+
+#define PP2_HDR_LEN_UNSPEC     (PP2_HEADER_LEN + PP2_ADDR_LEN_UNSPEC)
+#define PP2_HDR_LEN_INET       (PP2_HEADER_LEN + PP2_ADDR_LEN_INET)
+#define PP2_HDR_LEN_INET6      (PP2_HEADER_LEN + PP2_ADDR_LEN_INET6)
+#define PP2_HDR_LEN_UNIX       (PP2_HEADER_LEN + PP2_ADDR_LEN_UNIX)
+
+struct proxy_hdr_v2 {
+    uint8_t sig[PP2_SIGNATURE_LEN];    /* PP2_SIGNATURE */
+    uint8_t ver_cmd;                   /* protocol version | command */
+    uint8_t fam;                       /* protocol family and transport */
+    uint16_t len;                      /* length of remainder */
+    union {
+       struct {                        /* for TCP/UDP over IPv4, len = 12 */
+           uint32_t src_addr;
+           uint32_t dst_addr;
+           uint16_t src_port;
+           uint16_t dst_port;
+       }       ip4;
+       struct {                        /* for TCP/UDP over IPv6, len = 36 */
+           uint8_t src_addr[16];
+           uint8_t dst_addr[16];
+           uint16_t src_port;
+           uint16_t dst_port;
+       }       ip6;
+       struct {                        /* for AF_UNIX sockets, len = 216 */
+           uint8_t src_addr[108];
+           uint8_t dst_addr[108];
+       }       unx;
+    }       addr;
+};
+
+ /*
+  * End protocol v2 definitions from haproxy/include/types/connection.h.
+  */
+#endif /* HAPROXY_SRVR_INTERNAL */
+
 /* LICENSE
 /* .ad
 /* .fi
diff --git a/postfix/src/global/haproxy_srvr_test.c b/postfix/src/global/haproxy_srvr_test.c
new file mode 100644 (file)
index 0000000..fca1feb
--- /dev/null
@@ -0,0 +1,396 @@
+ /*
+  * Test program to exercise haproxy_srvr.c. See ptest_main.h for a
+  * documented example.
+  */
+
+ /*
+  * System library.
+  */
+#include <sys_defs.h>
+#include <string.h>
+
+ /*
+  * Utility library.
+  */
+#include <sock_addr.h>
+#include <vstring.h>
+
+ /*
+  * Global library.
+  */
+#define HAPROXY_SRVR_INTERNAL
+#include <haproxy_srvr.h>
+
+ /*
+  * Test library.
+  */
+#include <ptest.h>
+
+ /*
+  * Test cases with inputs and expected outputs. A request may contain
+  * trailing garbage, and it may be too short. A v1 request may also contain
+  * malformed address or port information.
+  */
+typedef struct BASE_TEST_CASE {
+    const char *haproxy_request;       /* v1 or v2 request including thrash */
+    ssize_t haproxy_req_len;           /* request length including thrash */
+    ssize_t want_req_len;              /* parsed request length */
+    int     want_non_proxy;            /* request is not proxied */
+    const char *want_return;           /* expected error string */
+    const char *want_client_addr;      /* expected client address string */
+    const char *want_server_addr;      /* expected client port string */
+    const char *want_client_port;      /* expected client address string */
+    const char *want_server_port;      /* expected server port string */
+} BASE_TEST_CASE;
+
+ /*
+  * Initialize the haproxy_request, haproxy_req_len, and want_req_len
+  * fields.
+  */
+#define STRING_AND_LENS(s) (s), (sizeof(s) - 1), (sizeof(s) - 1)
+
+ /*
+  * This table contains V1 protocol test cases that may also be converted
+  * into V2 protocol test cases.
+  */
+static BASE_TEST_CASE v1_test_cases[] = {
+    /* IPv6. */
+    {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1 123 321\n"), 0, 0, "fc::1:2:3:4", "fc::4:3:2:1", "123", "321"},
+    {STRING_AND_LENS("PROXY TCP6 FC:00:00:00:1:2:3:4 FC:00:00:00:4:3:2:1 123 321\n"), 0, 0, "fc::1:2:3:4", "fc::4:3:2:1", "123", "321"},
+    {STRING_AND_LENS("PROXY TCP6 1.2.3.4 4.3.2.1 123 321\n"), 0, "bad or missing client address"},
+    {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4 4.3.2.1 123 321\n"), 0, "bad or missing server address"},
+    /* IPv4 in IPv6. */
+    {STRING_AND_LENS("PROXY TCP6 ::ffff:1.2.3.4 ::ffff:4.3.2.1 123 321\n"), 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"},
+    {STRING_AND_LENS("PROXY TCP6 ::FFFF:1.2.3.4 ::FFFF:4.3.2.1 123 321\n"), 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"},
+    {STRING_AND_LENS("PROXY TCP4 ::ffff:1.2.3.4 ::ffff:4.3.2.1 123 321\n"), 0, "bad or missing client address"},
+    {STRING_AND_LENS("PROXY TCP4 1.2.3.4 ::ffff:4.3.2.1 123 321\n"), 0, "bad or missing server address"},
+    /* IPv4. */
+    {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123 321\n"), 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"},
+    {STRING_AND_LENS("PROXY TCP4 01.02.03.04 04.03.02.01 123 321\n"), 0, 0, "1.2.3.4", "4.3.2.1", "123", "321"},
+    {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123456 321\n"), 0, "bad or missing client port"},
+    {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123 654321\n"), 0, "bad or missing server port"},
+    {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 0123 321\n"), 0, "bad or missing client port"},
+    {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123 0321\n"), 0, "bad or missing server port"},
+    /* Missing fields. */
+    {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1 123\n"), 0, "bad or missing server port"},
+    {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4 fc:00:00:00:4:3:2:1\n"), 0, "bad or missing client port"},
+    {STRING_AND_LENS("PROXY TCP6 fc:00:00:00:1:2:3:4\n"), 0, "bad or missing server address"},
+    {STRING_AND_LENS("PROXY TCP6\n"), 0, "bad or missing client address"},
+    {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1 123\n"), 0, "bad or missing server port"},
+    {STRING_AND_LENS("PROXY TCP4 1.2.3.4 4.3.2.1\n"), 0, "bad or missing client port"},
+    {STRING_AND_LENS("PROXY TCP4 1.2.3.4\n"), 0, "bad or missing server address"},
+    {STRING_AND_LENS("PROXY TCP4\n"), 0, "bad or missing client address"},
+    /* Other. */
+    {STRING_AND_LENS("PROXY BLAH\n"), 0, "bad or missing protocol type"},
+    {STRING_AND_LENS("PROXY\n"), 0, "short protocol header"},
+    {"BLAH\n", 0, 0, 0, "short protocol header"},
+    {"\n", 0, 0, 0, "short protocol header"},
+    {"", 0, 0, 0, "short protocol header"},
+    0,
+};
+
+ /*
+  * Limited V2-only tests. XXX Should we add error cases? Several errors are
+  * already tested with mutations of V2 handshakes that were generated from
+  * V1 handshakes.
+  */
+static struct proxy_hdr_v2 v2_local_request = {
+    PP2_SIGNATURE, PP2_VERSION | PP2_CMD_LOCAL,
+};
+static BASE_TEST_CASE v2_non_proxy_test = {
+    (char *) &v2_local_request, PP2_HEADER_LEN, PP2_HEADER_LEN, 1,
+};
+
+#define STR_OR_NULL(s) ((s) ? (s) : "(null)")
+#define STR(x)         vstring_str(x)
+#define LEN(x)         VSTRING_LEN(x)
+
+/* evaluate_test_case - evaluate one test case (base or mutation) */
+
+static void evaluate_test_case(PTEST_CTX *t,const char *test_label,
+                                     const BASE_TEST_CASE *test_case)
+{
+    PTEST_RUN(t, test_label, {
+       const char *got_return;
+       ssize_t got_req_len;
+       int    got_non_proxy;
+       MAI_HOSTADDR_STR got_smtp_client_addr;
+       MAI_HOSTADDR_STR got_smtp_server_addr;
+       MAI_SERVPORT_STR got_smtp_client_port;
+       MAI_SERVPORT_STR got_smtp_server_port;
+
+       if (msg_verbose)
+           msg_info("test case=%s want_client_addr=%s want_server_addr=%s "
+                    "want_client_port=%s want_server_port=%s",
+                    test_label, STR_OR_NULL(test_case->want_client_addr),
+                    STR_OR_NULL(test_case->want_server_addr),
+                    STR_OR_NULL(test_case->want_client_port),
+                    STR_OR_NULL(test_case->want_server_port));
+
+       /*
+        * Start the test.
+        */
+       got_req_len = test_case->haproxy_req_len;
+       got_return =
+           haproxy_srvr_parse(test_case->haproxy_request, &got_req_len,
+                              &got_non_proxy,
+                              &got_smtp_client_addr, &got_smtp_client_port,
+                              &got_smtp_server_addr, &got_smtp_server_port);
+       if (strcmp(STR_OR_NULL(got_return), STR_OR_NULL(test_case->want_return))) {
+           ptest_error(t, "haproxy_srvr_parse return got=%s want=%s",
+                      STR_OR_NULL(got_return),
+                      STR_OR_NULL(test_case->want_return));
+           ptest_return(t);
+       }
+       if (got_req_len != test_case->want_req_len) {
+           ptest_error(t, "haproxy_srvr_parse str_len got=%ld want=%ld",
+                      (long) got_req_len,
+                      (long) test_case->want_req_len);
+           ptest_return(t);
+       }
+       if (got_non_proxy != test_case->want_non_proxy) {
+           ptest_error(t, "haproxy_srvr_parse non_proxy got=%d want=%d",
+                       got_non_proxy,
+                      test_case->want_non_proxy);
+           ptest_return(t);
+       }
+       if (test_case->want_non_proxy || test_case->want_return != 0)
+           /* No expected address/port results. */
+           ptest_return(t);
+
+       /*
+        * Compare address/port results against expected results.
+        */
+       if (strcmp(test_case->want_client_addr, got_smtp_client_addr.buf)) {
+           ptest_error(t, "haproxy_srvr_parse client_addr got=%s want=%s",
+                      got_smtp_client_addr.buf,
+                      test_case->want_client_addr);
+       }
+       if (strcmp(test_case->want_server_addr, got_smtp_server_addr.buf)) {
+           ptest_error(t, "haproxy_srvr_parse server_addr got=%s want=%s",
+                      got_smtp_server_addr.buf,
+                      test_case->want_server_addr);
+       }
+       if (strcmp(test_case->want_client_port, got_smtp_client_port.buf)) {
+           ptest_error(t, "haproxy_srvr_parse client_port got=%s want=%s",
+                      got_smtp_client_port.buf,
+                      test_case->want_client_port);
+       }
+       if (strcmp(test_case->want_server_port, got_smtp_server_port.buf)) {
+           ptest_error(t, "haproxy_srvr_parse server_port got=%s want=%s",
+                      got_smtp_server_port.buf,
+                      test_case->want_server_port);
+       }
+    });
+}
+
+/* convert_v1_proxy_req_to_v2 - convert well-formed v1 proxy request to v2 */
+
+static void convert_v1_proxy_req_to_v2(PTEST_CTX * t, VSTRING *buf,
+                                          const char *req, ssize_t req_len)
+{
+    const char myname[] = "convert_v1_proxy_req_to_v2";
+    const char *err;
+    int     non_proxy;
+    MAI_HOSTADDR_STR smtp_client_addr;
+    MAI_SERVPORT_STR smtp_client_port;
+    MAI_HOSTADDR_STR smtp_server_addr;
+    MAI_SERVPORT_STR smtp_server_port;
+    struct proxy_hdr_v2 *hdr_v2;
+    struct addrinfo *src_res;
+    struct addrinfo *dst_res;
+
+    /*
+     * Allocate buffer space for the largest possible protocol header, so we
+     * don't have to worry about hidden realloc() calls.
+     */
+    VSTRING_RESET(buf);
+    VSTRING_SPACE(buf, sizeof(struct proxy_hdr_v2));
+    hdr_v2 = (struct proxy_hdr_v2 *) STR(buf);
+
+    /*
+     * Fill in the header,
+     */
+    memcpy(hdr_v2->sig, PP2_SIGNATURE, PP2_SIGNATURE_LEN);
+    hdr_v2->ver_cmd = PP2_VERSION | PP2_CMD_PROXY;
+    if ((err = haproxy_srvr_parse(req, &req_len, &non_proxy, &smtp_client_addr,
+                                 &smtp_client_port, &smtp_server_addr,
+                                 &smtp_server_port)) != 0 || non_proxy)
+       ptest_fatal(t, "%s: malformed or non-proxy request: %s",
+                   myname, req);
+
+    if (hostaddr_to_sockaddr(smtp_client_addr.buf, smtp_client_port.buf, 0,
+                            &src_res) != 0)
+       ptest_fatal(t, "%s: unable to convert source address %s port %s",
+                   myname, smtp_client_addr.buf, smtp_client_port.buf);
+    if (hostaddr_to_sockaddr(smtp_server_addr.buf, smtp_server_port.buf, 0,
+                            &dst_res) != 0)
+       ptest_fatal(t, "%s: unable to convert destination address %s port %s",
+                   myname, smtp_server_addr.buf, smtp_server_port.buf);
+    if (src_res->ai_family != dst_res->ai_family)
+       ptest_fatal(t, "%s: mixed source/destination address families", myname);
+#ifdef AF_INET6
+    if (src_res->ai_family == PF_INET6) {
+       hdr_v2->fam = PP2_FAM_INET6 | PP2_TRANS_STREAM;
+       hdr_v2->len = htons(PP2_ADDR_LEN_INET6);
+       memcpy(hdr_v2->addr.ip6.src_addr,
+              &SOCK_ADDR_IN6_ADDR(src_res->ai_addr),
+              sizeof(hdr_v2->addr.ip6.src_addr));
+       hdr_v2->addr.ip6.src_port = SOCK_ADDR_IN6_PORT(src_res->ai_addr);
+       memcpy(hdr_v2->addr.ip6.dst_addr,
+              &SOCK_ADDR_IN6_ADDR(dst_res->ai_addr),
+              sizeof(hdr_v2->addr.ip6.dst_addr));
+       hdr_v2->addr.ip6.dst_port = SOCK_ADDR_IN6_PORT(dst_res->ai_addr);
+    } else
+#endif
+    if (src_res->ai_family == PF_INET) {
+       hdr_v2->fam = PP2_FAM_INET | PP2_TRANS_STREAM;
+       hdr_v2->len = htons(PP2_ADDR_LEN_INET);
+       hdr_v2->addr.ip4.src_addr = SOCK_ADDR_IN_ADDR(src_res->ai_addr).s_addr;
+       hdr_v2->addr.ip4.src_port = SOCK_ADDR_IN_PORT(src_res->ai_addr);
+       hdr_v2->addr.ip4.dst_addr = SOCK_ADDR_IN_ADDR(dst_res->ai_addr).s_addr;
+       hdr_v2->addr.ip4.dst_port = SOCK_ADDR_IN_PORT(dst_res->ai_addr);
+    } else {
+       ptest_fatal(t, "unknown address family 0x%x", src_res->ai_family);
+    }
+    vstring_set_payload_size(buf, PP2_SIGNATURE_LEN + ntohs(hdr_v2->len));
+    freeaddrinfo(src_res);
+    freeaddrinfo(dst_res);
+}
+
+static void run_test_variants(PTEST_CTX *t, BASE_TEST_CASE *v1_test_case)
+{
+    VSTRING *test_label;
+    BASE_TEST_CASE v2_test_case;
+    BASE_TEST_CASE mutated_test_case;
+    VSTRING *v2_request_buf;
+    VSTRING *mutated_request_buf;
+
+    test_label = vstring_alloc(100);
+    v2_request_buf = vstring_alloc(100);
+    mutated_request_buf = vstring_alloc(100);
+
+    /*
+     * Evaluate each v1 test case.
+     */
+    vstring_sprintf(test_label, "v1 baseline (%sformed)",
+                   v1_test_case->want_return ? "mal" : "well");
+    evaluate_test_case(t, STR(test_label), v1_test_case);
+
+    /*
+     * If the v1 test input is malformed, skip the mutation tests.
+     */
+    if (v1_test_case->want_return != 0)
+       ptest_return(t);
+
+    /*
+     * Mutation test: a well-formed v1 test case should still pass after
+     * appending a byte, and should return the actual parsed header length.
+     * The test uses the implicit VSTRING null safety byte.
+     */
+    vstring_sprintf(test_label, "v1 mutated (one byte appended)");
+       mutated_test_case = *v1_test_case;
+    mutated_test_case.haproxy_req_len += 1;
+    /* reuse v1_test_case->want_req_len */
+    evaluate_test_case(t, STR(test_label), &mutated_test_case);
+
+    /*
+     * Mutation test: a well-formed v1 test case should fail after stripping
+     * the terminator.
+     */
+    vstring_sprintf(test_label, "v1 mutated (last byte stripped)");
+    mutated_test_case = *v1_test_case;
+    mutated_test_case.want_return = "missing protocol header terminator";
+    mutated_test_case.haproxy_req_len -= 1;
+    mutated_test_case.want_req_len = mutated_test_case.haproxy_req_len;
+    evaluate_test_case(t, STR(test_label), &mutated_test_case);
+
+    /*
+     * A 'well-formed' v1 test case should pass after conversion to v2.
+     */
+    vstring_sprintf(test_label, "v2 baseline (converted from v1)");
+    v2_test_case = *v1_test_case;
+    convert_v1_proxy_req_to_v2(t, v2_request_buf,
+                              v1_test_case->haproxy_request,
+                              v1_test_case->haproxy_req_len);
+    v2_test_case.haproxy_request = STR(v2_request_buf);
+    v2_test_case.haproxy_req_len = PP2_HEADER_LEN
+       + ntohs(((struct proxy_hdr_v2 *) STR(v2_request_buf))->len);
+    v2_test_case.want_req_len = v2_test_case.haproxy_req_len;
+    evaluate_test_case(t, STR(test_label), &v2_test_case);
+
+    /*
+     * Mutation test: a well-formed v2 test case should still pass after
+     * appending a byte, and should return the actual parsed header length.
+     * The test uses the implicit VSTRING null safety byte.
+     */
+    vstring_sprintf(test_label, "v2 mutated (one byte appended)");
+    mutated_test_case = v2_test_case;
+    mutated_test_case.haproxy_req_len += 1;
+    /* reuse v2_test_case->want_req_len */
+    evaluate_test_case(t, STR(test_label), &mutated_test_case);
+
+    /*
+     * Mutation test: a well-formed v2 test case should fail after stripping
+     * one byte
+     */
+    vstring_sprintf(test_label, "v2 mutated (last byte stripped)");
+    mutated_test_case = v2_test_case;
+    mutated_test_case.haproxy_req_len -= 1;
+    mutated_test_case.want_req_len = mutated_test_case.haproxy_req_len;
+    mutated_test_case.want_return = "short version 2 protocol header";
+    evaluate_test_case(t, STR(test_label), &mutated_test_case);
+
+    /*
+     * Clean up.
+     */
+    vstring_free(v2_request_buf);
+    vstring_free(mutated_request_buf);
+    vstring_free(test_label);
+}
+
+ /*
+  * PTEST adapter.
+  */
+typedef struct PTEST_CASE {
+    const char *testname;
+    void    (*action) (PTEST_CTX *, const struct PTEST_CASE *);
+} PTEST_CASE;
+
+static void run_proxy_tests(PTEST_CTX *t, const PTEST_CASE *unused)
+{
+    BASE_TEST_CASE *v1_test_case;
+    VSTRING *test_label;
+
+    /*
+     * Run all variants of one base case test together in a subtest.
+     */
+    test_label = vstring_alloc(100);
+    for (v1_test_case = v1_test_cases;
+        v1_test_case->haproxy_request != 0; v1_test_case++) {
+       vstring_sprintf(test_label, "%d",
+                       1 + (int) (v1_test_case - v1_test_cases));
+       PTEST_RUN(t, STR(test_label), {
+           run_test_variants(t, v1_test_case);
+       });
+    }
+
+    /*
+     * Additional V2-only test.
+     */
+    vstring_sprintf(test_label, "%d",
+                   1 + (int) (v1_test_case - v1_test_cases));
+    PTEST_RUN(t, STR(test_label), {
+       evaluate_test_case(t, "v2 non-proxy request", &v2_non_proxy_test);
+    });
+    vstring_free(test_label);
+}
+
+ /*
+  * PTEST adapter.
+  */
+static const PTEST_CASE ptestcases[] = {
+    "haproxy_srvr_test", run_proxy_tests,
+};
+
+#include <ptest_main.h>
index c5f59c2d70ddee1861d14e75848a96fe64939e79..315a2e15d11ad5b90c34c5ce2ffa693923698aa5 100644 (file)
@@ -141,7 +141,7 @@ extern char *mail_pathname(const char *, const char *);
 #define MAIL_ATTR_PROTO_VERIFY "address_verification_prrotocol"
 
  /*
-  * Attribute names.
+  * Attribute names in internal and policy delegation protocols.
   */
 #define MAIL_ATTR_REQ          "request"
 #define MAIL_ATTR_NREQ         "nrequest"
@@ -201,6 +201,7 @@ extern char *mail_pathname(const char *, const char *);
 #define MAIL_ATTR_CRYPTO_CIPHER        "encryption_cipher"
 #define MAIL_ATTR_CRYPTO_KEYSIZE "encryption_keysize"
 #define MAIL_ATTR_COMPAT_LEVEL "compatibility_level"
+#define MAIL_ATTR_MAIL_VERSION "mail_version"
 
  /*
   * Suffixes for sender_name, sender_domain etc.
index 49f073faa6ea00a462bb91889189facac51d3e87..820708a2ded0dab7aa1b7edf4bc4ed95a8c47e4a 100644 (file)
@@ -20,7 +20,7 @@
   * Patches change both the patchlevel and the release date. Snapshots have no
   * patchlevel; they change the release date only.
   */
-#define MAIL_RELEASE_DATE      "20220724"
+#define MAIL_RELEASE_DATE      "20220816"
 #define MAIL_VERSION_NUMBER    "3.8"
 
 #ifdef SNAPSHOT
index ef8f88c7c64a2f7e499999e58214605d33925792..974443adfc44693965b402920b14cd7200bba55f 100644 (file)
@@ -128,7 +128,6 @@ static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE *unused)
 
     for (tp = test_cases; tp->map_spec; tp++) {
        vstring_sprintf(test_label, "test %d", (int) (tp - test_cases));
-/**INDENT** Error@126: Unbalanced parens */
        PTEST_RUN(t, STR(test_label), {
            for (cpp = tp->want_log; cpp < tp->want_log + MAX_WANT_LOG && *cpp; cpp++)
                expect_ptest_log_event(t, *cpp);
@@ -171,7 +170,6 @@ static void test_map_search(PTEST_CTX *t, const struct PTEST_CASE *unused)
                            escape_order(want_escaped,
                                     string_or_null(tp->exp_search_order)));
            }
-/**INDENT** Warning@168: Extra ) */
        });
     }
     vstring_free(want_escaped);
index 034c01be78cd4ac11e6cf9589b6b5b210d85c112..509e7ab133c2aaa24d9163b71e7b8742296edd30 100644 (file)
@@ -10,11 +10,12 @@ OBJS        = postscreen.o postscreen_dict.o postscreen_dnsbl.o \
        postscreen_starttls.o postscreen_expand.o postscreen_endpt.o \
        postscreen_haproxy.o
 HDRS   = 
-TESTSRC        =
+TESTSRC        = postscreen_dnsbl_test.c
 DEFS   = -I. -I$(INC_DIR) -D$(SYSTYPE)
 CFLAGS = $(DEBUG) $(OPT) $(DEFS)
-TESTPROG=
+TESTPROG= postscreen_dnsbl_test
 PROG   = postscreen
+TEST_LIB= ../../lib/libtesting.a ../../lib/libptest.a
 INC_DIR = ../../include
 LIBS   = ../../lib/lib$(LIB_PREFIX)master$(LIB_SUFFIX) \
        ../../lib/lib$(LIB_PREFIX)tls$(LIB_SUFFIX) \
@@ -34,7 +35,7 @@ Makefile: Makefile.in
 
 test:  $(TESTPROG)
 
-tests: test
+tests: test_postscreen_dnsbl
 
 root_tests:
 
@@ -59,6 +60,14 @@ clean:
 
 tidy:  clean
 
+postscreen_dnsbl_test: postscreen_dnsbl_test.o postscreen_dnsbl.o \
+       ../../lib//mock_server.o $(TEST_LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o postscreen_dnsbl.o \
+       ../../lib//mock_server.o $(TEST_LIB) $(LIBS) $(SYSLIBS)
+       
+test_postscreen_dnsbl: update postscreen_dnsbl_test 
+       $(SHLIB_ENV) ${VALGRIND} ./postscreen_dnsbl_test
+
 depend: $(MAKES)
        (sed '1,/^# do not edit/!d' Makefile.in; \
        set -e; for i in [a-z][a-z0-9]*.c; do \
@@ -156,6 +165,42 @@ postscreen_dnsbl.o: ../../include/vstring.h
 postscreen_dnsbl.o: ../../include/wrap_netdb.h
 postscreen_dnsbl.o: postscreen.h
 postscreen_dnsbl.o: postscreen_dnsbl.c
+postscreen_dnsbl_test.o: ../../include/addr_match_list.h
+postscreen_dnsbl_test.o: ../../include/argv.h
+postscreen_dnsbl_test.o: ../../include/attr.h
+postscreen_dnsbl_test.o: ../../include/check_arg.h
+postscreen_dnsbl_test.o: ../../include/connect.h
+postscreen_dnsbl_test.o: ../../include/dict.h
+postscreen_dnsbl_test.o: ../../include/dict_cache.h
+postscreen_dnsbl_test.o: ../../include/events.h
+postscreen_dnsbl_test.o: ../../include/htable.h
+postscreen_dnsbl_test.o: ../../include/iostuff.h
+postscreen_dnsbl_test.o: ../../include/mail_params.h
+postscreen_dnsbl_test.o: ../../include/mail_proto.h
+postscreen_dnsbl_test.o: ../../include/make_attr.h
+postscreen_dnsbl_test.o: ../../include/maps.h
+postscreen_dnsbl_test.o: ../../include/match_list.h
+postscreen_dnsbl_test.o: ../../include/mock_server.h
+postscreen_dnsbl_test.o: ../../include/msg.h
+postscreen_dnsbl_test.o: ../../include/msg_output.h
+postscreen_dnsbl_test.o: ../../include/msg_vstream.h
+postscreen_dnsbl_test.o: ../../include/myaddrinfo.h
+postscreen_dnsbl_test.o: ../../include/myflock.h
+postscreen_dnsbl_test.o: ../../include/mymalloc.h
+postscreen_dnsbl_test.o: ../../include/nvtable.h
+postscreen_dnsbl_test.o: ../../include/pmock_expect.h
+postscreen_dnsbl_test.o: ../../include/ptest.h
+postscreen_dnsbl_test.o: ../../include/ptest_main.h
+postscreen_dnsbl_test.o: ../../include/server_acl.h
+postscreen_dnsbl_test.o: ../../include/string_list.h
+postscreen_dnsbl_test.o: ../../include/stringops.h
+postscreen_dnsbl_test.o: ../../include/sys_defs.h
+postscreen_dnsbl_test.o: ../../include/vbuf.h
+postscreen_dnsbl_test.o: ../../include/vstream.h
+postscreen_dnsbl_test.o: ../../include/vstring.h
+postscreen_dnsbl_test.o: ../../include/wrap_netdb.h
+postscreen_dnsbl_test.o: postscreen.h
+postscreen_dnsbl_test.o: postscreen_dnsbl_test.c
 postscreen_early.o: ../../include/addr_match_list.h
 postscreen_early.o: ../../include/argv.h
 postscreen_early.o: ../../include/check_arg.h
index 69a5e17503dc644ae27d828670b3c7178780cb53..b9ceeeb99b2b3c9809b0b8da27f6fe18b108b674 100644 (file)
@@ -485,6 +485,7 @@ const char *psc_maps_find(MAPS *, const char *, int);
 extern void psc_dnsbl_init(void);
 extern int psc_dnsbl_retrieve(const char *, const char **, int, int *);
 extern int psc_dnsbl_request(const char *, void (*) (int, void *), void *);
+extern void psc_dnsbl_deinit(void);
 
  /*
   * postscreen_tests.c
index 7d9a5e94b98beb0d0e28d42b281caf4be214cb64..c726f5478e239eaf0f291ac0ecbcbe8f8643b17f 100644 (file)
 /*
 /*     int     psc_dnsbl_retrieve(client_addr, dnsbl_name, dnsbl_index,
 /*                                     dnsbl_ttl)
-/*     char    *client_addr;
+/*     const char *client_addr;
 /*     const char **dnsbl_name;
 /*     int     dnsbl_index;
 /*     int     *dnsbl_ttl;
+/* AUXILIARY FUNCTIONS
+/*     void    psc_dnsbl_deinit(void)
 /* DESCRIPTION
 /*     This module implements preliminary support for DNSBL lookups.
 /*     Multiple requests for the same information are handled with
@@ -44,6 +46,9 @@
 /*     reference count. The reply TTL value is clamped to
 /*     postscreen_dnsbl_min_ttl and postscreen_dnsbl_max_ttl.  It
 /*     is an error to retrieve a score without requesting it first.
+/*
+/*     psc_dnsbl_deinit() tries to reset state so that psc_dnsbl_init()
+/*     can be called again. This is to support tests only.
 /* LICENSE
 /* .ad
 /* .fi
@@ -115,10 +120,23 @@ static HTABLE *dnsbl_site_cache;  /* indexed by DNSBNL domain */
 static HTABLE_INFO **dnsbl_site_list;  /* flattened cache */
 
 typedef struct {
-    const char *safe_dnsbl;            /* from postscreen_dnsbl_reply_map */
+    char   *safe_dnsbl;                        /* from postscreen_dnsbl_reply_map */
     struct PSC_DNSBL_SITE *first;      /* list of (filter, weight) tuples */
 } PSC_DNSBL_HEAD;
 
+static void psc_dnsbl_site_free(void *ptr);
+
+static void psc_dnsbl_head_free(void *ptr)
+{
+    PSC_DNSBL_HEAD *head = (PSC_DNSBL_HEAD *) ptr;
+
+    if (head->safe_dnsbl)
+       myfree(head->safe_dnsbl);
+    if (head->first)
+       psc_dnsbl_site_free(head->first);
+    myfree(head);
+};
+
 typedef struct PSC_DNSBL_SITE {
     char   *filter;                    /* printable filter (default: null) */
     char   *byte_codes;                        /* encoded filter (default: null) */
@@ -126,6 +144,19 @@ typedef struct PSC_DNSBL_SITE {
     struct PSC_DNSBL_SITE *next;       /* linked list */
 } PSC_DNSBL_SITE;
 
+static void psc_dnsbl_site_free(void *ptr)
+{
+    PSC_DNSBL_SITE *site = (PSC_DNSBL_SITE *) ptr;
+
+    if (site->filter)
+       myfree(site->filter);
+    if (site->byte_codes)
+       myfree(site->byte_codes);
+    if (site->next)
+       psc_dnsbl_site_free(site->next);
+    myfree(site);
+}
+
  /*
   * Per-client DNSBL scores.
   * 
@@ -162,6 +193,13 @@ typedef struct {
     PSC_CALL_BACK_ENTRY table[1];      /* actually a bunch */
 } PSC_DNSBL_SCORE;
 
+static void psc_dnsbl_score_free(void *ptr)
+{
+    PSC_DNSBL_SCORE *score = (PSC_DNSBL_SCORE *) ptr;
+
+    myfree(score);
+}
+
 #define PSC_CALL_BACK_INIT(sp) do { \
        (sp)->limit = 0; \
        (sp)->index = 0; \
@@ -197,8 +235,10 @@ typedef struct {
     } while (0)
 
 #define PSC_CALL_BACK_NOTIFY(sp, ev) do { \
+       PSC_CALL_BACK_ENTRY *_table_ = (sp)->table; \
+       int _index_ = (sp)->index; \
        PSC_CALL_BACK_ENTRY *_cb_; \
-       for (_cb_ = (sp)->table; _cb_ < (sp)->table + (sp)->index; _cb_++) \
+       for (_cb_ = _table_; _cb_ < _table_ + _index_; _cb_++) \
            if (_cb_->callback != 0) \
                _cb_->callback((ev), _cb_->context); \
     } while (0)
@@ -231,7 +271,7 @@ static void psc_dnsbl_add_site(const char *site)
     int     weight;
     HTABLE_INFO *ht;
     char   *parse_err;
-    const char  *safe_dnsbl;
+    const char *safe_dnsbl;
 
     /*
      * Parse the required DNSBL domain name, the optional reply filter and
@@ -480,6 +520,8 @@ static void psc_dnsbl_receive(int event, void *context)
     vstream_fclose(stream);
 }
 
+static int request_count;
+
 /* psc_dnsbl_request  - send dnsbl query, increment reference count */
 
 int     psc_dnsbl_request(const char *client_addr,
@@ -492,7 +534,6 @@ int     psc_dnsbl_request(const char *client_addr,
     HTABLE_INFO **ht;
     PSC_DNSBL_SCORE *score;
     HTABLE_INFO *hash_node;
-    static int request_count;
 
     /*
      * Some spambots make several connections at nearly the same time,
@@ -621,4 +662,42 @@ void    psc_dnsbl_init(void)
     reply_client = vstring_alloc(100);
     reply_dnsbl = vstring_alloc(100);
     reply_addr = vstring_alloc(100);
+
+    /*
+     * Reset the request ID seed, to make tests predictable.
+     */
+    request_count = 0;
+}
+
+/* psc_dnsbl_deinit - helper for tests only */
+
+void    psc_dnsbl_deinit(void)
+{
+    if (psc_dnsbl_service) {
+       myfree(psc_dnsbl_service);
+       psc_dnsbl_service = 0;
+    }
+    if (dnsbl_site_cache) {
+       htable_free(dnsbl_site_cache, psc_dnsbl_head_free);
+       dnsbl_site_cache = 0;
+    }
+    if (dnsbl_site_list) {
+       myfree(dnsbl_site_list);
+       dnsbl_site_list = 0;
+    }
+    if (dnsbl_score_cache) {
+       htable_free(dnsbl_score_cache, psc_dnsbl_score_free);
+       dnsbl_score_cache = 0;
+    }
+    if (reply_client) {
+       vstring_free(reply_client);
+       reply_client = 0;
+    }
+    if (reply_dnsbl) {
+       vstring_free(reply_dnsbl), reply_dnsbl = 0;
+    }
+    if (reply_addr) {
+       vstring_free(reply_addr);
+       reply_addr = 0;
+    }
 }
diff --git a/postfix/src/postscreen/postscreen_dnsbl_test.c b/postfix/src/postscreen/postscreen_dnsbl_test.c
new file mode 100644 (file)
index 0000000..71a275e
--- /dev/null
@@ -0,0 +1,563 @@
+ /*
+  * Test program to exercise postscreen_dnsbl.c. See comments in
+  * mock_server.c, and PTEST_README for documented examples of unit tests.
+  */
+
+ /*
+  * System library.
+  */
+#include <sys_defs.h>
+#include <limits.h>
+
+ /*
+  * Utility library.
+  */
+#include <attr.h>
+#include <events.h>
+#include <vstream.h>
+#include <vstring.h>
+
+ /*
+  * Global library.
+  */
+#include <mail_proto.h>
+#include <mail_params.h>
+
+ /*
+  * Test library.
+  */
+#include <ptest.h>
+#include <make_attr.h>
+#include <mock_server.h>
+
+ /*
+  * Application-specific.
+  */
+#include <postscreen.h>
+
+ /*
+  * Generic case structure.
+  */
+typedef struct PTEST_CASE {
+    const char *testname;              /* Human-readable description */
+    void    (*action) (PTEST_CTX *t, const struct PTEST_CASE *);
+} PTEST_CASE;
+
+ /*
+  * Structure to capture postscreen_dnsbl_retrieve() inputs and outputs.
+  */
+struct session_state {
+    /* postscreen_dnsbl_retrieve() inputs. */
+    const char *req_addr;              /* Client IP address */
+    int     req_idx;                   /* Request index */
+    /* postscreen_dnsbl_retrieve()  outputs. */
+    const char *got_dnsbl;             /* Null, or biggest contributor */
+    int     got_ttl;                   /* TTL from A or SOA record */
+    int     got_score;                 /* Combined score */
+};
+
+ /*
+  * Surrogates for global variables used, but not defined, by
+  * postscreen_dnsbl.c.
+  */
+int     var_psc_dnsbl_min_ttl;         /* postscreen_dnsbl_min_ttl */
+int     var_psc_dnsbl_max_ttl;         /* postscreen_dnsbl_max_ttl */
+int     var_psc_dnsbl_tmout;           /* postscreen_dnsbl_timeout */
+char   *var_psc_dnsbl_sites;           /* postscreen_dnsbl_sites */
+char   *var_dnsblog_service;           /* dnsblog_service_name */
+DICT   *psc_dnsbl_reply;               /* postscreen_dnsbl_reply_map */
+
+/* deinit_psc_globals - best effort reset */
+
+static void deinit_psc_globals(void)
+{
+
+    /*
+     * deinit_psc_globals() must be idempotent, so that it can be called
+     * safely at the start and end of each test.
+     */
+    if (var_psc_dnsbl_sites) {
+       myfree(var_psc_dnsbl_sites);
+       var_psc_dnsbl_sites = 0;
+    }
+    if (psc_dnsbl_reply) {
+       dict_close(psc_dnsbl_reply);
+       psc_dnsbl_reply = 0;
+    }
+
+    /*
+     * Reset postscreen_dnsbl.c internals.
+     */
+    psc_dnsbl_deinit();
+}
+
+/* init_psc_globals - initialize globals */
+
+static void init_psc_globals(const char *dnsbl_sites)
+{
+
+    /*
+     * We call deinit_psc_globals() first, because it may not be called at
+     * the end of a failed test. A test failure should not affect later
+     * tests.
+     */
+    deinit_psc_globals();
+
+    /*
+     * Set parameters that postscreen_dnsbl.c depends on.
+     */
+    var_psc_dnsbl_min_ttl = 60;
+    var_psc_dnsbl_max_ttl = 3600;
+    var_psc_dnsbl_tmout = atoi(DEF_PSC_DNSBL_TMOUT);
+    var_psc_dnsbl_sites = mystrdup(dnsbl_sites);
+    var_dnsblog_service = DEF_DNSBLOG_SERVICE;
+
+    /*
+     * postscreen_dnsbl.c mandatory initialization.
+     */
+    psc_dnsbl_init();
+}
+
+/* psc_dnsbl_callback - event handler to retrieve score and ttl */
+
+static void psc_dnsbl_callback(int event, void *context)
+{
+    struct session_state *sp = (struct session_state *) context;
+
+    sp->got_score = psc_dnsbl_retrieve(sp->req_addr, &sp->got_dnsbl,
+                                      sp->req_idx, &sp->got_ttl);
+}
+
+ /*
+  * Test data and tests for a single reputation provider.
+  */
+struct single_dnsbl_data {
+    const char *label;                 /* test label */
+    const char *dnsbl_sites;           /* postscreen_dnsbl_sites */
+    const char *req_dnsbl;             /* in dnsblog request */
+    const char *req_addr;              /* in dnsblog request */
+    const char *res_addr;              /* in dnsblog response */
+    int     res_ttl;                   /* in dnsblog response */
+    int     want_score;                        /* sum of weights */
+};
+
+static const struct single_dnsbl_data single_dnsbl_tests[] = {
+    {
+       "single site listed address",
+        /* dnsbl_sites */ "zen.spamhaus.org",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "127.0.0.2",
+        /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10",
+        /* res_ttl */ 60,
+        /* want_score */ 1,
+    },
+    {
+       "repeated site 1x rpc 2x score",
+        /* dnsbl_sites */ "zen.spamhaus.org, zen.spamhaus.org",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "127.0.0.2",
+        /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10",
+        /* res_ttl */ 60,
+        /* want_score */ 2,
+    },
+    {
+       "unlisted address zero score",
+        /* dnsbl_sites */ "zen.spamhaus.org",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "127.0.0.1",
+        /* res_addr */ "",
+        /* res_ttl */ 60,
+        /* want_score */ 0,
+    },
+    {
+       "site with weight first",
+        /* dnsbl_sites */ "zen.spamhaus.org*3, zen.spamhaus.org",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "127.0.0.2",
+        /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10",
+        /* res_ttl */ 60,
+        /* want_score */ 4,
+    },
+    {
+       "site with weight last",
+        /* dnsbl_sites */ "zen.spamhaus.org, zen.spamhaus.org*3",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "127.0.0.2",
+        /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10",
+        /* res_ttl */ 60,
+        /* want_score */ 4,
+    },
+    {
+       "site with filter+weight first",
+        /* dnsbl_sites */ "zen.spamhaus.org=127.0.0.10*3, zen.spamhaus.org",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "127.0.0.2",
+        /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10",
+        /* res_ttl */ 60,
+        /* want_score */ 4,
+    },
+    {
+       "site with filter+weight last",
+        /* dnsbl_sites */ "zen.spamhaus.org, zen.spamhaus.org=127.0.0.10*3",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "127.0.0.2",
+        /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10",
+        /* res_ttl */ 60,
+        /* want_score */ 4,
+    },
+    {
+       "filter+weight add and subtract",
+        /* dnsbl_sites */ "zen.spamhaus.org=127.0.0.[1..255]*3, zen.spamhaus.org=127.0.0.3*-1",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "10.2.3.4",
+        /* res_addr */ "127.0.0.3 127.0.0.10",
+        /* res_ttl */ 60,
+        /* want_score */ 2,
+    },
+    {
+       "filter+weight add and not subtract",
+        /* dnsbl_sites */ "zen.spamhaus.org=127.0.0.[1..255]*3, zen.spamhaus.org=127.0.0.3*-1",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "10.2.3.4",
+        /* res_addr */ "127.0.0.10",
+        /* res_ttl */ 60,
+        /* want_score */ 3,
+    },
+};
+
+static void test_single_dnsbl(PTEST_CTX *t, const PTEST_CASE *tp)
+{
+    MOCK_SERVER *mp;
+    struct session_state session_state;
+    const char *dnsblog_path = "private/dnsblog";
+    VSTRING *serialized_req;
+    VSTRING *serialized_resp;
+    const int request_id = 0;
+    const struct single_dnsbl_data *tt;
+
+    for (tt = single_dnsbl_tests; tt < single_dnsbl_tests
+        + PTEST_NROF(single_dnsbl_tests); tt++) {
+       if (tt->label == 0)
+           ptest_fatal(t, "Null test label in single_dnsbl_tests array!");
+       PTEST_RUN(t, tt->label, {
+
+           /*
+            * Reset global state and parameters used by postscreen_dnsbl.c.
+            */
+           init_psc_globals(tt->dnsbl_sites);
+
+           /*
+            * Instantiate a mock server.
+            */
+           mp = mock_unix_server_create(dnsblog_path);
+
+           /*
+            * Set up the expected dnsblog request, and the corresponding
+            * response. The mock dnsblog server immediately generates a read
+            * event request, so we should send something soon.
+            */
+           serialized_req =
+               make_attr(ATTR_FLAG_NONE,
+                         SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, tt->req_dnsbl),
+                         SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR,
+                                       tt->req_addr),
+                         SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id),
+                         ATTR_TYPE_END);
+           serialized_resp =
+               make_attr(ATTR_FLAG_NONE,
+                         SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, tt->req_dnsbl),
+                         SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR,
+                                       tt->req_addr),
+                         SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id),
+                         SEND_ATTR_STR(MAIL_ATTR_RBL_ADDR, tt->res_addr),
+                         SEND_ATTR_INT(MAIL_ATTR_TTL, tt->res_ttl),
+                         ATTR_TYPE_END);
+           mock_server_interact(mp, serialized_req, serialized_resp);
+
+           /*
+            * Send a request by calling psc_dnsbl_request(), and run the
+            * event loop once to notify the mock dnsblog server that a
+            * request is pending. The mock dnsblog server will receive the
+            * request, and if it matches the expected request, the mock
+            * dnsblog server will immediately send the prepared response.
+            */
+           session_state.req_addr = tt->req_addr;
+           session_state.got_dnsbl = 0;
+           session_state.got_ttl = INT_MAX;
+           session_state.got_score = INT_MAX;
+           session_state.req_idx = psc_dnsbl_request(tt->req_addr,
+                                                     psc_dnsbl_callback,
+                                                     &session_state);
+           event_loop(2);
+
+           /*
+            * Run the event loop another time to wake up
+            * psc_dnsbl_receive(). That function will deserialize the mock
+            * dnsblog server's response, and will immediately call our
+            * psc_dnsbl_callback() function to store the result into the
+            * session_state object.
+            */
+           event_loop(2);
+
+           /*
+            * Validate the response.
+            */
+           if (session_state.got_ttl == INT_MAX) {
+               ptest_error(t, "psc_dnsbl_callback() was not called, "
+                           "or did not update the session_state");
+           } else {
+               if (session_state.got_ttl != tt->res_ttl)
+                   ptest_error(t, "unexpected ttl: got %d, want %d",
+                               session_state.got_ttl, tt->res_ttl);
+               if (session_state.got_score != tt->want_score)
+                   ptest_error(t, "unexpected score: got %d, want %d",
+                               session_state.got_score, tt->want_score);
+           }
+
+           /*
+            * Clean up.
+            */
+           vstring_free(serialized_req);
+           vstring_free(serialized_resp);
+           mock_server_free(mp);
+           deinit_psc_globals();
+       });
+    }
+}
+
+ /*
+  * Test data and tests for multiple reputation providers.
+  */
+struct dnsbl_data {
+    const char *req_dnsbl;             /* in dnsblog request */
+    const char *res_addr;              /* in dnsblog response */
+    int     res_ttl;                   /* in dnsblog response */
+};
+
+#define MAX_DNSBL_SITES        3
+
+struct multi_dnsbl_data {
+    const char *label;                 /* test label */
+    const char *dnsbl_sites;           /* postscreen_dnsbl_sites */
+    const char *req_addr;              /* in dnsblog request */
+    struct dnsbl_data dnsbl_data[MAX_DNSBL_SITES];
+    int            want_ttl;                   /* effective TTL */
+    int     want_score;                        /* sum of weights */
+};
+
+static const struct multi_dnsbl_data multi_dnsbl_tests[] = {
+    {
+       "dual dnsbl, listed by both",
+        /* dnsbl_sites */ "zen.spamhaus.org, foo.example.org",
+        /* req_addr */ "10.2.3.4", {
+           {
+                /* req_dnsbl */ "foo.example.org",
+                /* res_addr */ "127.0.0.10",
+                /* res_ttl */ 60,
+           },
+           {
+                /* req_dnsbl */ "zen.spamhaus.org",
+                /* res_addr */ "127.0.0.10",
+                /* res_ttl */ 60,
+           },
+       },
+        /* want_ttl */ 60,
+        /* want_score */ 2,
+    }, {
+       "dual dnsbl, listed by first",
+        /* dnsbl_sites */ "zen.spamhaus.org, foo.example.org",
+        /* req_addr */ "10.2.3.4", {
+           {
+                /* req_dnsbl */ "foo.example.org",
+                /* res_addr */ "",
+                /* res_ttl */ 62,
+           },
+           {
+                /* req_dnsbl */ "zen.spamhaus.org",
+                /* res_addr */ "127.0.0.10",
+                /* res_ttl */ 61,
+           },
+       },
+        /* want_ttl */ 61,
+        /* want_score */ 1,
+    }, {
+       "dual dnsbl, listed by last",
+        /* dnsbl_sites */ "zen.spamhaus.org, foo.example.org",
+        /* req_addr */ "10.2.3.4", {
+           {
+                /* req_dnsbl */ "foo.example.org",
+                /* res_addr */ "127.0.0.10",
+                /* res_ttl */ 62,
+           },
+           {
+                /* req_dnsbl */ "zen.spamhaus.org",
+                /* res_addr */ "",
+                /* res_ttl */ 61,
+           },
+       },
+        /* want_ttl */ 62,
+        /* want_score */ 1,
+    }, {
+       "dual dnsbl, unlisted address zero score",
+        /* dnsbl_sites */ "zen.spamhaus.org, foo.example.org",
+        /* req_addr */ "10.2.3.4", {
+           {
+                /* req_dnsbl */ "foo.example.org",
+                /* res_addr */ "",
+                /* res_ttl */ 62,
+           },
+           {
+                /* req_dnsbl */ "zen.spamhaus.org",
+                /* res_addr */ "",
+                /* res_ttl */ 61,
+           },
+       },
+        /* want_ttl */ 61,
+        /* want_score */ 0,
+    }, {
+       "dual dnsbl, allowlist wins",
+        /* dnsbl_sites */ "list.dnswl.org=127.0.[0..255].[1..3]*-2, foo.example.org",
+        /* req_addr */ "10.2.3.4", {
+           {
+                /* req_dnsbl */ "foo.example.org",
+                /* res_addr */ "127.0.0.10",
+                /* res_ttl */ 62,
+           },
+           {
+                /* req_dnsbl */ "list.dnswl.org",
+                /* res_addr */ "127.0.5.2",
+                /* res_ttl */ 61,
+           },
+       },
+        /* want_ttl */ 61,
+        /* want_score */ -1,
+    }
+};
+
+static void test_multi_dnsbl(PTEST_CTX *t, const PTEST_CASE *tp)
+{
+    MOCK_SERVER *mp[MAX_DNSBL_SITES];
+    struct session_state session_state;
+    const char *dnsblog_path = "private/dnsblog";
+    const int request_id = 0;
+    const struct multi_dnsbl_data *tt;
+    const struct dnsbl_data *dp;
+    int     n;
+
+    for (tt = multi_dnsbl_tests; tt < multi_dnsbl_tests
+        + PTEST_NROF(multi_dnsbl_tests); tt++) {
+       if (tt->label == 0)
+           ptest_fatal(t, "Null test label in multi_dnsbl_tests array!");
+       PTEST_RUN(t, tt->label, {
+
+           /*
+            * Reset global state and parameters used by postscreen_dnsbl.c.
+            */
+           init_psc_globals(tt->dnsbl_sites);
+
+           for (n = 0, dp = tt->dnsbl_data; n < MAX_DNSBL_SITES
+                && dp[n].req_dnsbl != 0; n++) {
+               VSTRING *serialized_req;
+               VSTRING *serialized_resp;
+
+               /*
+                * Instantiate a mock server.
+                */
+               if ((mp[n] = mock_unix_server_create(dnsblog_path)) == 0)
+                   ptest_fatal(t, "mock_unix_server_create: %m");
+
+               /*
+                * Set up the expected dnsblog requests, and the
+                * corresponding responses. The mock dnsblog server
+                * immediately generates read event requests, so we should
+                * send something soon.
+                */
+               serialized_req =
+                   make_attr(ATTR_FLAG_NONE,
+                             SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN,
+                                           dp[n].req_dnsbl),
+                             SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR,
+                                           tt->req_addr),
+                             SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id),
+                             ATTR_TYPE_END);
+               serialized_resp =
+                   make_attr(ATTR_FLAG_NONE,
+                             SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN,
+                                           dp[n].req_dnsbl),
+                             SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR,
+                                           tt->req_addr),
+                             SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id),
+                             SEND_ATTR_STR(MAIL_ATTR_RBL_ADDR,
+                                           dp[n].res_addr),
+                             SEND_ATTR_INT(MAIL_ATTR_TTL,
+                                           dp[n].res_ttl),
+                             ATTR_TYPE_END);
+               mock_server_interact(mp[n], serialized_req,
+                                         serialized_resp);
+               vstring_free(serialized_req);
+               vstring_free(serialized_resp);
+           }
+
+           /*
+            * Send a request by calling psc_dnsbl_request(), and run the
+            * event loop once to notify the mock dnsblog servers that a
+            * request is pending. Each mock dnsblog server will receive a
+            * request, and if it matches the expected request, the mock
+            * dnsblog server will immediately send the prepared response.
+            */
+           session_state.req_addr = tt->req_addr;
+           session_state.got_dnsbl = 0;
+           session_state.got_ttl = INT_MAX;
+           session_state.got_score = INT_MAX;
+           session_state.req_idx = psc_dnsbl_request(tt->req_addr,
+                                                     psc_dnsbl_callback,
+                                                     &session_state);
+           event_loop(2);
+
+           /*
+            * Run the event loop again, to wake up psc_dnsbl_receive(). That
+            * function will deserialize the mock dnsblog server's response,
+            * and will immediately call our psc_dnsbl_callback() function to
+            * store the result into the session_state object.
+            */
+           event_loop(2);
+
+           /*
+            * Validate the response.
+            */
+           if (session_state.got_ttl == INT_MAX) {
+               ptest_error(t, "psc_dnsbl_callback() was not called, "
+                           "or did not update the session_state");
+           } else {
+               if (session_state.got_ttl != tt->want_ttl)
+                   ptest_error(t, "unexpected ttl: got %d, want %d",
+                               session_state.got_ttl, tt->want_ttl);
+               if (session_state.got_score != tt->want_score)
+                   ptest_error(t, "unexpected score: got %d, want %d",
+                               session_state.got_score, tt->want_score);
+           }
+
+           /*
+            * Clean up.
+            */
+           for (n = 0, dp = tt->dnsbl_data; n < MAX_DNSBL_SITES
+                && dp[n].req_dnsbl != 0; n++)
+               mock_server_free(mp[n]);
+           deinit_psc_globals();
+       });
+    }
+}
+
+ /*
+  * Test cases.
+  */
+const PTEST_CASE ptestcases[] = {
+    {
+       "single dnsbl", test_single_dnsbl,
+    },
+    {
+       "multi dnsbl", test_multi_dnsbl,
+    },
+};
+
+#include <ptest_main.h>
index ae7b57bc9a28ac3689bf9687a371c0d5a751337b..e2561016a7053c5b4cea09e143b70d8baed998f1 100644 (file)
@@ -113,6 +113,11 @@ extern void ptest_defer(PTEST_CTX *, PTEST_DEFER_FN, void *);
     t = parent; \
 } while (0)
 
+ /*
+  * How many elements in a test case array.
+  */
+#define PTEST_NROF(x) (sizeof(x)/sizeof((x)[0]))
+
 /* LICENSE
 /* .ad
 /* .fi
index ff2d5112acd80c4e6c1bbf4fda2fb60b84e8962f..b35907310562e2b113ce62b77078836b8f96121e 100644 (file)
@@ -129,6 +129,14 @@ int     main(int argc, char **argv)
     const PTEST_CASE *tp;
     int     fail;
 
+    /*
+     * This must be set BEFORE the first hash table call.
+     */
+#ifndef DORANDOMIZE
+    if (putenv("NORANDOMIZE=") != 0)
+       msg_fatal("putenv() failed: %m");
+#endif
+
     /*
      * Send msg(3) logging to stderr by default.
      */
@@ -147,9 +155,7 @@ int     main(int argc, char **argv)
      * data, instead of having to store all test data in a PTEST_CASE
      * structure.
      */
-#define NROF(x) (sizeof(x)/sizeof((x)[0]))
-
-    for (tp = ptestcases; tp < ptestcases + NROF(ptestcases); tp++) {
+    for (tp = ptestcases; tp < ptestcases + PTEST_NROF(ptestcases); tp++) {
        if (tp->testname == 0)
            msg_fatal("Null testname in ptestcases array!");
        PTEST_RUN(t, tp->testname, {
index 66c35cabccb9c653d16a158ae8d79bc5ce7ef412..52622bcd00460f3a14d3a6ba63cd95b954848f89 100644 (file)
 /*
 /*     void    ptest_defer(
 /*     PTEST_CTX *t,
-/*     void    (*defer_fn)(void *)
+/*     void    (*defer_fn)(void *),
 /*     void    *defer_ctx)
 /* DESCRIPTION
 /*     PTEST_RUN() is called from inside a test to run a subtest.
 /*
 /*     PTEST_RUN() is a macro that runs the { body_in_braces }
-/*     with msg(3) logging temporarily redirected to a buffer, and
+/*     with msg(3) logging temporarily redirected to a listener, and
 /*     with panic, fatal, error, and non-error functions that
 /*     terminate a test without terminating the process.
 /*
index f7d7bbb6a19c511ddcdec4ebcac72de90916bd8c..61bb6cda4f0a9ae6962eedc7c4c45c906c4fdafc 100644 (file)
@@ -342,6 +342,7 @@ smtpd_check.o: ../../include/mail_error.h
 smtpd_check.o: ../../include/mail_params.h
 smtpd_check.o: ../../include/mail_proto.h
 smtpd_check.o: ../../include/mail_stream.h
+smtpd_check.o: ../../include/mail_version.h
 smtpd_check.o: ../../include/map_search.h
 smtpd_check.o: ../../include/maps.h
 smtpd_check.o: ../../include/match_list.h
index 2785ce1fc732a6d30cc1dcbc583e99c9d06edb64..29e8671a41c593b18dbb3970a550e4250fd78a9b 100644 (file)
 #include <attr_override.h>
 #include <map_search.h>
 #include <info_log_addr_form.h>
+#include <mail_version.h>
 
 /* Application-specific. */
 
@@ -4101,6 +4102,8 @@ static int check_policy_service(SMTPD_STATE *state, const char *server,
                                        policy_clnt->policy_context),
                          SEND_ATTR_STR(MAIL_ATTR_COMPAT_LEVEL,
                                        var_compatibility_level),
+                         SEND_ATTR_STR(MAIL_ATTR_MAIL_VERSION,
+                                       var_mail_version),
                          ATTR_TYPE_END,
                          ATTR_FLAG_MISSING,    /* Reply attributes. */
                          RECV_ATTR_STR(MAIL_ATTR_ACTION, action),
index 6a66712006adac48f743c1522881c9bbe26b8b7c..09cde8bd2136b50de1db6117fdf28b612fe77f99 100644 (file)
@@ -1,20 +1,25 @@
 SHELL  = /bin/sh
 SRCS   = mock_myaddrinfo.c mock_dns_lookup.c mock_servent.c \
        mock_getaddrinfo.c match_basic.c match_addr.c make_addr.c \
-       addrinfo_to_string.c 
-LIB_OBJ        = match_basic.o match_addr.o make_addr.o addrinfo_to_string.o
-MOCK_OBJ= mock_myaddrinfo.o mock_dns_lookup.o mock_servent.o mock_getaddrinfo.o
+       addrinfo_to_string.c make_attr.c match_attr.c
+LIB_OBJ        = match_basic.o match_addr.o make_addr.o addrinfo_to_string.o \
+       make_attr.o match_attr.o
+MOCK_OBJ= mock_myaddrinfo.o mock_dns_lookup.o mock_servent.o \
+       mock_getaddrinfo.o mock_server.o
 TEST_OBJ = mock_dns_lookup_test.o mock_getaddrinfo_test.o \
-       mock_myaddrinfo_test.o mock_servent_test.o match_addr_test.o
+       mock_myaddrinfo_test.o mock_servent_test.o match_addr_test.o \
+       mock_server_test.o match_attr_test.o
 HDRS   = mock_myaddrinfo.h mock_dns.h mock_servent.h mock_getaddrinfo.h \
-       match_basic.h match_addr.h make_addr.h addrinfo_to_string.h
+       match_basic.h match_addr.h make_addr.h addrinfo_to_string.h \
+       mock_server.h make_attr.h match_attr.h
 TESTSRC        =
 DEFS   = -I. -I$(INC_DIR) -D$(SYSTYPE)
 CFLAGS = $(DEBUG) $(OPT) $(DEFS)
 INCL   =
 LIB    = libtesting.a
 TESTPROG= mock_dns_lookup_test mock_getaddrinfo_test \
-       mock_myaddrinfo_test mock_servent_test match_addr_test 
+       mock_myaddrinfo_test mock_servent_test match_addr_test \
+       match_attr_test mock_server_test
 
 LIBS    = ../../lib/libptest.a \
        ../../lib/lib$(LIB_PREFIX)dns$(LIB_SUFFIX) \
@@ -60,7 +65,7 @@ clean:
 tidy:   clean
 
 tests: test_mock_dns_lookup test_mock_getaddrinfo test_mock_myaddrinfo \
-       test_mock_servent test_match_addr 
+       test_mock_servent test_match_addr test_mock_server
 
 mock_myaddrinfo_test: mock_myaddrinfo_test.o mock_myaddrinfo.o $(LIB) $(LIBS)
        $(CC) $(CFLAGS) -o $@ $@.o mock_myaddrinfo.o $(LIB) $(LIBS) $(SYSLIBS)
@@ -92,6 +97,18 @@ match_addr_test: match_addr_test.o match_addr.o $(LIB) $(LIBS)
 test_match_addr: update match_addr_test
        $(SHLIB_ENV) ${VALGRIND} ./match_addr_test
 
+mock_server_test: mock_server_test.o mock_server.o $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o mock_server.o $(LIB) $(LIBS) $(SYSLIBS)
+
+test_mock_server: update mock_server_test
+       $(SHLIB_ENV) ${VALGRIND} ./mock_server_test
+
+match_attr_test: match_attr_test.o match_attr.o $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o match_attr.o $(LIB) $(LIBS) $(SYSLIBS)
+
+test_match_attr: update match_attr_test
+       $(SHLIB_ENV) ${VALGRIND} ./match_attr_test
+
 depend: $(MAKES)
        (sed '1,/^# do not edit/!d' Makefile.in; \
        set -e; for i in [a-z][a-z0-9]*.c; do \
@@ -119,6 +136,20 @@ make_addr.o: ../../include/sys_defs.h
 make_addr.o: ../../include/wrap_netdb.h
 make_addr.o: make_addr.c
 make_addr.o: make_addr.h
+make_attr.o: ../../include/argv.h
+make_attr.o: ../../include/attr.h
+make_attr.o: ../../include/check_arg.h
+make_attr.o: ../../include/htable.h
+make_attr.o: ../../include/msg.h
+make_attr.o: ../../include/mymalloc.h
+make_attr.o: ../../include/nvtable.h
+make_attr.o: ../../include/ptest.h
+make_attr.o: ../../include/sys_defs.h
+make_attr.o: ../../include/vbuf.h
+make_attr.o: ../../include/vstream.h
+make_attr.o: ../../include/vstring.h
+make_attr.o: make_attr.c
+make_attr.o: make_attr.h
 match_addr.o: ../../include/argv.h
 match_addr.o: ../../include/check_arg.h
 match_addr.o: ../../include/msg.h
@@ -149,6 +180,40 @@ match_addr_test.o: ../../include/wrap_netdb.h
 match_addr_test.o: make_addr.h
 match_addr_test.o: match_addr.h
 match_addr_test.o: match_addr_test.c
+match_attr.o: ../../include/argv.h
+match_attr.o: ../../include/attr.h
+match_attr.o: ../../include/check_arg.h
+match_attr.o: ../../include/htable.h
+match_attr.o: ../../include/msg.h
+match_attr.o: ../../include/mymalloc.h
+match_attr.o: ../../include/nvtable.h
+match_attr.o: ../../include/ptest.h
+match_attr.o: ../../include/sys_defs.h
+match_attr.o: ../../include/vbuf.h
+match_attr.o: ../../include/vstream.h
+match_attr.o: ../../include/vstring.h
+match_attr.o: match_attr.c
+match_attr.o: match_attr.h
+match_attr_test.o: ../../include/argv.h
+match_attr_test.o: ../../include/attr.h
+match_attr_test.o: ../../include/check_arg.h
+match_attr_test.o: ../../include/htable.h
+match_attr_test.o: ../../include/msg.h
+match_attr_test.o: ../../include/msg_output.h
+match_attr_test.o: ../../include/msg_vstream.h
+match_attr_test.o: ../../include/mymalloc.h
+match_attr_test.o: ../../include/nvtable.h
+match_attr_test.o: ../../include/pmock_expect.h
+match_attr_test.o: ../../include/ptest.h
+match_attr_test.o: ../../include/ptest_main.h
+match_attr_test.o: ../../include/stringops.h
+match_attr_test.o: ../../include/sys_defs.h
+match_attr_test.o: ../../include/vbuf.h
+match_attr_test.o: ../../include/vstream.h
+match_attr_test.o: ../../include/vstring.h
+match_attr_test.o: make_attr.h
+match_attr_test.o: match_attr.h
+match_attr_test.o: match_attr_test.c
 match_basic.o: ../../include/argv.h
 match_basic.o: ../../include/check_arg.h
 match_basic.o: ../../include/msg.h
@@ -304,3 +369,41 @@ mock_servent_test.o: ../../include/vstring.h
 mock_servent_test.o: ../../include/wrap_netdb.h
 mock_servent_test.o: mock_servent.h
 mock_servent_test.o: mock_servent_test.c
+mock_server.o: ../../include/argv.h
+mock_server.o: ../../include/check_arg.h
+mock_server.o: ../../include/connect.h
+mock_server.o: ../../include/events.h
+mock_server.o: ../../include/iostuff.h
+mock_server.o: ../../include/msg.h
+mock_server.o: ../../include/mymalloc.h
+mock_server.o: ../../include/ptest.h
+mock_server.o: ../../include/sys_defs.h
+mock_server.o: ../../include/vbuf.h
+mock_server.o: ../../include/vstream.h
+mock_server.o: ../../include/vstring.h
+mock_server.o: match_attr.h
+mock_server.o: mock_server.c
+mock_server.o: mock_server.h
+mock_server_test.o: ../../include/argv.h
+mock_server_test.o: ../../include/attr.h
+mock_server_test.o: ../../include/check_arg.h
+mock_server_test.o: ../../include/connect.h
+mock_server_test.o: ../../include/events.h
+mock_server_test.o: ../../include/htable.h
+mock_server_test.o: ../../include/iostuff.h
+mock_server_test.o: ../../include/mail_proto.h
+mock_server_test.o: ../../include/msg.h
+mock_server_test.o: ../../include/msg_output.h
+mock_server_test.o: ../../include/msg_vstream.h
+mock_server_test.o: ../../include/mymalloc.h
+mock_server_test.o: ../../include/nvtable.h
+mock_server_test.o: ../../include/pmock_expect.h
+mock_server_test.o: ../../include/ptest.h
+mock_server_test.o: ../../include/ptest_main.h
+mock_server_test.o: ../../include/stringops.h
+mock_server_test.o: ../../include/sys_defs.h
+mock_server_test.o: ../../include/vbuf.h
+mock_server_test.o: ../../include/vstream.h
+mock_server_test.o: ../../include/vstring.h
+mock_server_test.o: mock_server.h
+mock_server_test.o: mock_server_test.c
diff --git a/postfix/src/testing/make_attr.c b/postfix/src/testing/make_attr.c
new file mode 100644 (file)
index 0000000..1fd944b
--- /dev/null
@@ -0,0 +1,60 @@
+/*++
+/* NAME
+/*     make_attr 3
+/* SUMMARY
+/*     create serialized attribute request or response
+/* SYNOPSIS
+/*     #include <make_attr.h>
+/*
+/*     VSTRING *make_attr(int flags, ...)
+/* DESCRIPTION
+/*     make_attr() creates a serialized request or response attribute
+/*     list. The arguments are like attr_print().
+/* LICENSE
+/* .ad
+/* .fi
+/*     The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/*     Wietse Venema
+/*     Google, Inc.
+/*     111 8th Avenue
+/*     New York, NY 10011, USA
+/*--*/
+
+ /*
+  * System library.
+  */
+#include <sys_defs.h>
+#include <stdarg.h>
+
+ /*
+  * Utility library.
+  */
+#include <attr.h>
+#include <vstring.h>
+
+ /*
+  * Test library.
+  */
+#include <ptest.h>
+#include <make_attr.h>
+
+/* make_attr - serialize attribute list */
+
+VSTRING *make_attr(int flags,...)
+{
+    static const char myname[] = "make_attr";
+    VSTRING *res = vstring_alloc(100);
+    VSTREAM *stream;
+    va_list ap;
+    int     err;
+
+    if ((stream = vstream_memopen(res, O_WRONLY)) == 0)
+       ptest_fatal(ptest_ctx_current(), "%s: vstream_memopen: %m", myname);;
+    va_start(ap, flags);
+    err = attr_vprint(stream, flags, ap);
+    va_end(ap);
+    if (vstream_fclose(stream) != 0 || err)
+       ptest_fatal(ptest_ctx_current(), "%s: write attributes: %m", myname);
+    return (res);
+}
diff --git a/postfix/src/testing/make_attr.h b/postfix/src/testing/make_attr.h
new file mode 100644 (file)
index 0000000..97659d9
--- /dev/null
@@ -0,0 +1,35 @@
+#ifndef _MAKE_ATTR_H_INCLUDED_
+#define _MAKE_ATTR_H_INCLUDED_
+
+/*++
+/* NAME
+/*     make_attr 3h
+/* SUMMARY
+/*     create serialized attributes
+/* SYNOPSIS
+/*     #include <make_attr.h>
+/* DESCRIPTION
+/* .nf
+
+ /*
+  * Utility library.
+  */
+#include <vstring.h>
+
+ /*
+  * External interface.
+  */
+extern VSTRING *make_attr(int,...);
+
+/* LICENSE
+/* .ad
+/* .fi
+/*     The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/*     Wietse Venema
+/*     Google, Inc.
+/*     111 8th Avenue
+/*     New York, NY 10011, USA
+/*--*/
+
+#endif
index c577468c6558c439fcda12c73565523e91f08136..12f8b5c472fa024102085f1ed528e83f86d3301a 100644 (file)
@@ -25,6 +25,31 @@ typedef struct PTEST_CASE {
     void    (*action) (PTEST_CTX *t, const struct PTEST_CASE *);
 } PTEST_CASE;
 
+static void test_eq_addrinfo_equal(PTEST_CTX *t, const PTEST_CASE *unused)
+{
+    struct addrinfo hints;
+    struct addrinfo *want_addrinfo;
+
+    /*
+     * Set up expectations.
+     */
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family = PF_INET;
+    hints.ai_socktype = SOCK_STREAM;
+    want_addrinfo = make_addrinfo(&hints, "localhost", "127.0.0.1", 25);
+
+    /*
+     * Verify that this addrinfo matches itself.
+     */
+    if (!eq_addrinfo(t, "addrinfo", want_addrinfo, want_addrinfo))
+       ptest_error(t, "eq_addrinfo() returned false for identical objects");
+
+    /*
+     * Clean up.
+     */
+    freeaddrinfo(want_addrinfo);
+}
+
 static void test_eq_addrinfo_diff(PTEST_CTX *t, const PTEST_CASE *unused)
 {
     struct addrinfo hints;
@@ -121,6 +146,9 @@ static void test_eq_sockaddr_diff(PTEST_CTX *t, const PTEST_CASE *unused)
   * Test cases.
   */
 const PTEST_CASE ptestcases[] = {
+    {
+       "Compare equal IPv4 addrinfos", test_eq_addrinfo_equal,
+    },
     {
        "Compare different IPv4 addrinfos", test_eq_addrinfo_diff,
     },
diff --git a/postfix/src/testing/match_attr.c b/postfix/src/testing/match_attr.c
new file mode 100644 (file)
index 0000000..bd36760
--- /dev/null
@@ -0,0 +1,159 @@
+/*++
+/* NAME
+/*     match_attr 3
+/* SUMMARY
+/*     matchers for network address information
+/* SYNOPSIS
+/*     #include <match_attr.h>
+/*
+/*     int     eq_attr(
+/*     PTEST_CTX *t,
+/*     const char *what,
+/*     VSTRING *got,
+/*     VSTRING *want)
+/* DESCRIPTION
+/*     The functions described here are safe macros that include
+/*     call-site information (file name, line number) that may be
+/*     used in error messages.
+/*
+/*     eq_attr() compares two serialized attribute lists and returns
+/*     whether their arguments contain the same values. If the t
+/*     argument is not null, eq_attr() will report details with
+/*     ptest_error()).
+/* BUGS
+/*     An attribute name can appear only once in an attribute list.
+/* LICENSE
+/* .ad
+/* .fi
+/*     The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/*     Wietse Venema
+/*     Google, Inc.
+/*     111 8th Avenue
+/*     New York, NY 10011, USA
+/*--*/
+
+ /*
+  * System library.
+  */
+#include <sys_defs.h>
+#include <string.h>
+#include <stdlib.h>
+
+ /*
+  * Utility library.
+  */
+#include <attr.h>
+#include <htable.h>
+#include <vstring.h>
+
+ /*
+  * Test library.
+  */
+#include <ptest.h>
+#include <match_attr.h>
+
+/* compar -qsort callback */
+
+static int compar(const void *a, const void *b)
+{
+    return (strcmp((*(HTABLE_INFO **) a)->key, (*(HTABLE_INFO **) b)->key));
+}
+
+/* _eq_attr - match serialized attributes */
+
+int     _eq_attr(const char *file, int line, PTEST_CTX *t,
+                     const char *what, VSTRING *got_buf, VSTRING *want_buf)
+{
+    static const char myname[] = "eq_attr";
+    HTABLE *got_hash;
+    HTABLE *want_hash;
+    int     count;
+    VSTREAM *mp;
+    HTABLE_INFO **ht_list;
+    HTABLE_INFO **ht;
+    char   *ht_value;
+
+    if (VSTRING_LEN(got_buf) == VSTRING_LEN(want_buf)
+       && memcmp(vstring_str(got_buf), vstring_str(want_buf),
+                 VSTRING_LEN(got_buf)) == 0)
+       return (1);
+
+    if (t != 0) {
+
+       /*
+        * Deserialize the actual attributes into a hash. This loses order
+        * information.
+        */
+       got_hash = htable_create(13);
+       if ((mp = vstream_memopen(got_buf, O_RDONLY)) == 0)
+           ptest_fatal(t, "%s: vstream_memopen: %m", myname);
+       count = attr_scan(mp, ATTR_FLAG_NONE,
+                         ATTR_TYPE_HASH, got_hash,
+                         ATTR_TYPE_END);
+       if (vstream_fclose(mp) != 0 || count <= 0)
+           ptest_fatal(t, "%s: vstream_fclose: %m", myname);
+
+       /*
+        * Deserialize the wanted attributes into a hash. This loses order
+        * information.
+        */
+       want_hash = htable_create(13);
+       if ((mp = vstream_memopen(want_buf, O_RDONLY)) == 0)
+           ptest_fatal(t, "%s: vstream_memopen: %m", myname);
+       count = attr_scan(mp, ATTR_FLAG_NONE,
+                         ATTR_TYPE_HASH, want_hash,
+                         ATTR_TYPE_END);
+       if (vstream_fclose(mp) != 0 || count <= 0)
+           ptest_fatal(t, "%s: vstream_fclose: %m", myname);
+
+       /*
+        * Delete the intersection of the deserialized attribute lists.
+        */
+       ht_list = htable_list(got_hash);
+       for (ht = ht_list; *ht; ht++) {
+           if ((ht_value = htable_find(want_hash, ht[0]->key)) != 0
+               && strcmp(ht_value, ht[0]->value) == 0) {
+               htable_delete(want_hash, ht[0]->key, myfree);
+               /* At this point, ht_value is a dangling pointer. */
+               htable_delete(got_hash, ht[0]->key, myfree);
+               /* At this point, ht is a dangling pointer. */
+           }
+       }
+       myfree(ht_list);
+
+       /*
+        * If the attributes differ only in order, then say so. We have no
+        * order information. This should never happen with real requests and
+        * responses.
+        */
+       if (got_hash->used == 0 && want_hash->used == 0) {
+           ptest_error(t, "%s: attribute order differs", what);
+       }
+
+       /*
+        * List differences in name or value.
+        */
+       else {
+           ptest_error(t, "%s: attributes differ, +got/-want follows", what);
+
+           /*
+            * Enumerate the unique attributes.
+            */
+           ht_list = htable_list(got_hash);
+           qsort(ht_list, got_hash->used, sizeof(*ht_list), compar);
+           for (ht = ht_list; *ht; ht++)
+               ptest_error(t, "+%s = %s", ht[0]->key, (char *) ht[0]->value);
+           myfree(ht_list);
+
+           ht_list = htable_list(want_hash);
+           qsort(ht_list, want_hash->used, sizeof(*ht_list), compar);
+           for (ht = ht_list; *ht; ht++)
+               ptest_error(t, "-%s = %s", ht[0]->key, (char *) ht[0]->value);
+           myfree(ht_list);
+       }
+       htable_free(got_hash, myfree);
+       htable_free(want_hash, myfree);
+    }
+    return (0);
+}
diff --git a/postfix/src/testing/match_attr.h b/postfix/src/testing/match_attr.h
new file mode 100644 (file)
index 0000000..b91097d
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef _MATCH_ATTR_H_INCLUDED_
+#define _MATCH_ATTR_H_INCLUDED_
+
+/*++
+/* NAME
+/*     match_attr 3h
+/* SUMMARY
+/*     attribute matching
+/* SYNOPSIS
+/*     #include <match_attr.h>
+/* DESCRIPTION
+/* .nf
+
+ /*
+  * Utility library.
+  */
+#include <vstring.h>
+
+ /*
+  * Test library.
+  */
+#include <ptest.h>
+
+ /*
+  * External interface.
+  */
+extern int _eq_attr(const char *, int, PTEST_CTX *, const char *,
+                           VSTRING *, VSTRING *);
+
+#define eq_attr(t, what, got, want) \
+    _eq_attr(__FILE__, __LINE__, (t), (what), (got), (want))
+
+/* LICENSE
+/* .ad
+/* .fi
+/*     The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/*     Wietse Venema
+/*     Google, Inc.
+/*     111 8th Avenue
+/*     New York, NY 10011, USA
+/*--*/
+
+#endif
diff --git a/postfix/src/testing/match_attr_test.c b/postfix/src/testing/match_attr_test.c
new file mode 100644 (file)
index 0000000..c9fb540
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+  * Test program to exercise match_attr functions including logging. See
+  * documentation in PTEST_README for the structure of this file.
+  */
+
+ /*
+  * System library.
+  */
+#include <sys_defs.h>
+
+ /*
+  * Utility library.
+  */
+#include <attr.h>
+#include <vstring.h>
+
+ /*
+  * Test library.
+  */
+#include <make_attr.h>
+#include <match_attr.h>
+#include <ptest.h>
+
+typedef struct PTEST_CASE {
+    const char *testname;              /* Human-readable description */
+    void    (*action) (PTEST_CTX *t, const struct PTEST_CASE *);
+} PTEST_CASE;
+
+static void test_eq_attr_equal(PTEST_CTX *t, const PTEST_CASE *unused)
+{
+    VSTRING *want_attr;
+
+    /*
+     * Serialize some attributes.
+     */
+    want_attr = make_attr(ATTR_FLAG_NONE,
+                         SEND_ATTR_STR("this-key", "this-value"),
+                         SEND_ATTR_STR("that-key", "that-value"),
+                         ATTR_TYPE_END);
+
+    /*
+     * VERIFY that this serialized attribute list matches ifself.
+     */
+    if (!eq_attr(t, "want_attr", want_attr, want_attr))
+       ptest_fatal(t, "eq_attr() returned false for identical objects");
+
+    /*
+     * Clean up.
+     */
+    vstring_free(want_attr);
+}
+
+static void test_eq_attr_swapped(PTEST_CTX *t, const PTEST_CASE *unused)
+{
+    VSTRING *want_attr;
+    VSTRING *swapped_attr;
+
+    /*
+     * Serialize some attributes.
+     */
+    want_attr = make_attr(ATTR_FLAG_NONE,
+                         SEND_ATTR_STR("this-key", "this-value"),
+                         SEND_ATTR_STR("that-key", "that-value"),
+                         ATTR_TYPE_END);
+    swapped_attr = make_attr(ATTR_FLAG_NONE,
+                            SEND_ATTR_STR("that-key", "that-value"),
+                            SEND_ATTR_STR("this-key", "this-value"),
+                            ATTR_TYPE_END);
+
+    /*
+     * VERIFY that eq_attr() report attributes that differ only in order.
+     */
+    expect_ptest_error(t, "attribute order differs");
+    if (eq_attr(t, "want_attr", swapped_attr, want_attr))
+       ptest_fatal(t, "eq_attr() returned true for swapped objects");
+
+    /*
+     * Clean up.
+     */
+    vstring_free(want_attr);
+}
+
+static void test_eq_attr_diff(PTEST_CTX *t, const PTEST_CASE *unused)
+{
+    VSTRING *want_attr;
+    VSTRING *swapped_attr;
+
+    /*
+     * Serialize some attributes.
+     */
+    want_attr = make_attr(ATTR_FLAG_NONE,
+                         SEND_ATTR_STR("this-key", "this-value"),
+                         SEND_ATTR_STR("that-key", "that-value"),
+                         SEND_ATTR_STR("same-key", "same-value"),
+                         ATTR_TYPE_END);
+    swapped_attr = make_attr(ATTR_FLAG_NONE,
+                            SEND_ATTR_STR("not-this-key", "this-value"),
+                            SEND_ATTR_STR("that-key", "not-that-value"),
+                            SEND_ATTR_STR("same-key", "same-value"),
+                            ATTR_TYPE_END);
+
+    /*
+     * Verify that match_attr reports the expected differences.
+     */
+    expect_ptest_error(t, "attributes differ");
+    expect_ptest_error(t, "+not-this-key = this-value");
+    expect_ptest_error(t, "+that-key = not-that-value");
+    expect_ptest_error(t, "-that-key = that-value");
+    expect_ptest_error(t, "-this-key = this-value");
+    if (eq_attr(t, "want_attr", swapped_attr, want_attr))
+       ptest_fatal(t, "eq_attr() returned true for different objects");
+
+    /*
+     * Clean up.
+     */
+    vstring_free(want_attr);
+    vstring_free(swapped_attr);
+}
+
+ /*
+  * Test cases.
+  */
+const PTEST_CASE ptestcases[] = {
+    {
+       "Compare identical attribute lists", test_eq_attr_equal,
+    },
+    {
+       "Compare swapped attribute lists", test_eq_attr_swapped,
+    },
+    {
+       "Compare different attribute lists", test_eq_attr_diff,
+    },
+};
+
+#include <ptest_main.h>
diff --git a/postfix/src/testing/mock_server.c b/postfix/src/testing/mock_server.c
new file mode 100644 (file)
index 0000000..35db1d9
--- /dev/null
@@ -0,0 +1,331 @@
+/*++
+/* NAME
+/*     mock_server 3
+/* SUMMARY
+/*     Mock server for hermetic tests
+/* SYNOPSIS
+/*     #include <mock_server.h>
+/*
+/*     MOCK_SERVER *mock_unix_server_create(
+/*     const char *destination)
+/*
+/*     void    mock_server_interact(
+/*     MOCK_SERVER *server,
+/*     const VSTRING *request,
+/*     const VSTRING *response)
+/*
+/*     void    mock_server_free(MOCK_SERVER *server)
+/*
+/*     int     unix_connect(
+/*     const char *destination,
+/*     int     block_mode,
+/*     int     timeout)
+/* AUXILIARY FUNCTIONS
+/*     void    mock_server_free_void_ptr(void *ptr)
+/* DESCRIPTION
+/*     The purpose of this code is to make tests hermetic, i.e.
+/*     independent from a real server.
+/*
+/*     This module overrides the client function unix_connect()
+/*     with a function that connects to a mock server instance.
+/*     The mock server must be instantiated in advance with
+/*     mock_unix_server_create(). The connection destination name
+/*     is not associated with out-of-process resources.
+/*
+/*     mock_unix_server_create() creates a mock in-process server
+/*     that will "accept" one unix_connect() request with the
+/*     specified destination. To accept multiple connections, use
+/*     multiple mock_unix_server_create() calls.
+/*
+/*     mock_server_interact() sets up one expected request and/or
+/*     prepared response. Specify a null request to configure an
+/*     unconditional server response such as an initial handshake,
+/*     and specify a null response to specify a final request. If
+/*     an expected request is configured, the client should send
+/*     a request and call event_loop() once to deliver the request
+/*     to the mock server. If a prepared response is configured,
+/*     the mock server will respond immediately, and the client
+/*     should call event_loop() once to receive the response from
+/*     the server. To simulate multiple request/response interactions,
+/*     use a sequence of mock_server_interact() calls.
+/*
+/*     mock_server_free() destroys the specified server instance.
+/*     mock_server_free_void_ptr() provides an alternative API to
+/*     allow for systems where (struct *) and (void *) differ.
+/* BUGS
+/*     Each connection supports no more than one request and one
+/*     response at a time; each request and each response must fit
+/*     in a VSTREAM buffer (4096 bytes as of Postfix version
+/*     3.8), and must not be larger than SO_SNDBUF for AF_LOCAL
+/*     stream sockets (8192 bytes or more on contemporaneous
+/*     systems).
+/* LICENSE
+/* .ad
+/* .fi
+/*     The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/*     Wietse Venema
+/*     Google, Inc.
+/*     111 8th Avenue
+/*     New York, NY 10011, USA
+/*--*/
+
+ /*
+  * System library.
+  */
+#include <sys_defs.h>
+#include <sys/socket.h>
+#include <errno.h>
+#include <string.h>
+
+ /*
+  * Utility library.
+  */
+#include <events.h>
+#include <msg.h>
+#include <mymalloc.h>
+#include <vstring.h>
+
+ /*
+  * Test libraries
+  */
+#include <match_attr.h>
+#include <mock_server.h>
+#include <ptest.h>
+
+ /*
+  * Macros to make obscure code more readable.
+  */
+#define COPY_OR_NULL(dst, src) do { \
+       if ((src) != 0) { \
+           if ((dst) == 0) \
+               (dst) = vstring_alloc(LEN(src)); \
+           vstring_memcpy((dst), STR(src), LEN(src)); \
+       } else if ((dst) != 0) { \
+           vstring_free(dst); \
+           (dst) = 0; \
+       } \
+    } while (0)
+
+#define MOCK_SERVER_REQUEST_READ_EVENT(fd, action, context, timeout) do { \
+        if (msg_verbose > 1) \
+            msg_info("%s: read-request fd=%d", myname, (fd)); \
+        event_enable_read((fd), (action), (context)); \
+        event_request_timer((action), (context), (timeout)); \
+    } while (0)
+
+#define MOCK_SERVER_CLEAR_EVENT_REQUEST(fd, time_act, context) do { \
+        if (msg_verbose > 1) \
+            msg_info("%s: clear-request fd=%d", myname, (fd)); \
+        event_disable_readwrite(fd); \
+        event_cancel_timer((time_act), (context)); \
+    } while (0)
+
+
+#define MOCK_SERVER_TIMEOUT    10
+
+#define MOCK_SERVER_SIDE       (0)
+#define MOCK_CLIENT_SIDE       (1)
+
+ /*
+  * Other macros.
+  */
+#define STR    vstring_str
+#define LEN    VSTRING_LEN
+
+ /*
+  * List head. We could use a RING object and save a few bits in data space,
+  * at the cost of more complex code.
+  */
+static MOCK_SERVER mock_unix_server_head;
+
+/* mock_unix_server_create - instantiate an unconnected mock server */
+
+MOCK_SERVER *mock_unix_server_create(const char *dest)
+{
+    MOCK_SERVER *mp;
+
+    mp = (MOCK_SERVER *) mymalloc(sizeof(*mp));
+    if (socketpair(AF_LOCAL, SOCK_STREAM, 0, mp->fds) < 0) {
+       myfree(mp);
+       ptest_error(ptest_ctx_current(), "mock_unix_server_create(%s): "
+                   "socketpair(AF_LOCAL, SOCK_STREAM, 0, fds): %m", dest);
+       return (0);
+    }
+    mp->flags = 0;
+    mp->want_dest = mystrdup(dest);
+    mp->want_req = 0;
+    mp->resp = 0;
+    mp->iobuf = 0;
+
+    /*
+     * Link the mock server into the waiting list.
+     */
+    mp->next = mock_unix_server_head.next;
+    mock_unix_server_head.next = mp;
+    mp->prev = &mock_unix_server_head;
+    if (mp->next)
+       mp->next->prev = mp;
+    return (mp);
+}
+
+/* mock_unix_server_respond - send a prepared response */
+
+static void mock_unix_server_respond(MOCK_SERVER *mp)
+{
+    const char myname[] = "mock_unix_server_respond";
+    ssize_t got_len;
+
+    got_len = write(mp->fds[MOCK_SERVER_SIDE], STR(mp->resp), LEN(mp->resp));
+    if (got_len < 0)
+       ptest_fatal(ptest_ctx_current(), "%s: write: %m", myname);
+    if (got_len != LEN(mp->resp))
+       ptest_fatal(ptest_ctx_current(), "%s: wrote %ld of %ld bytes",
+                   myname, (long) got_len, (long) LEN(mp->resp));
+}
+
+/* mock_unix_server_read_event - receive request and respond */
+
+static void mock_unix_server_read_event(int event, void *context)
+{
+    const char myname[] = "mock_unix_server_read_event";
+    MOCK_SERVER *mp = (MOCK_SERVER *) context;
+    ssize_t peek_len;
+    ssize_t got_len;
+
+    /*
+     * Disarm this file descriptor.
+     */
+    MOCK_SERVER_CLEAR_EVENT_REQUEST(mp->fds[MOCK_SERVER_SIDE],
+                                   mock_unix_server_read_event,
+                                   context);
+
+    /*
+     * Handle the event.
+     */
+    switch (event) {
+    case EVENT_READ:
+       break;
+    case EVENT_TIME:
+       ptest_error(ptest_ctx_current(), "%s: timeout", myname);
+       return;
+    default:
+       ptest_fatal(ptest_ctx_current(), "%s: unexpected event: %d",
+                   myname, event);
+    }
+
+    /*
+     * Receive the request.
+     */
+    switch (peek_len = peekfd(mp->fds[MOCK_SERVER_SIDE])) {
+    case -1:
+       ptest_error(ptest_ctx_current(), "%s: read: %m", myname);
+       return;
+    case 0:
+       ptest_error(ptest_ctx_current(), "%s: read EOF", myname);
+       return;
+    default:
+       break;
+    }
+    if (mp->iobuf == 0)
+       mp->iobuf = vstring_alloc(1000);
+    VSTRING_SPACE(mp->iobuf, peek_len);
+    got_len = read(mp->fds[MOCK_SERVER_SIDE], STR(mp->iobuf), peek_len);
+    if (got_len != peek_len) {
+       ptest_fatal(ptest_ctx_current(), "%s: read %ld of %ld bytes",
+                   myname, (long) got_len, (long) peek_len);
+       return;
+    }
+    vstring_set_payload_size(mp->iobuf, got_len);
+    if (!eq_attr(ptest_ctx_current(), "request", mp->iobuf, mp->want_req))
+       return;
+
+    /*
+     * Send the response, if available.
+     */
+    else if (mp->resp)
+       mock_unix_server_respond(mp);
+}
+
+/* mock_server_interact - set up request and/or response */
+
+void    mock_server_interact(MOCK_SERVER *mp,
+                                         const VSTRING *req,
+                                         const VSTRING *resp)
+{
+    const char myname[] = "mock_server_interact";
+
+    if (req == 0 && resp == 0)
+       ptest_fatal(ptest_ctx_current(), "%s: null request and null response",
+                   myname);
+    COPY_OR_NULL(mp->want_req, req);
+    COPY_OR_NULL(mp->resp, resp);
+    if (req != 0) {
+       MOCK_SERVER_REQUEST_READ_EVENT(mp->fds[MOCK_SERVER_SIDE],
+                                      mock_unix_server_read_event,
+                                      (void *) mp, MOCK_SERVER_TIMEOUT);
+    } else {
+       mock_unix_server_respond(mp);
+    }
+}
+
+/* mock_unix_server_unlink - detach one instance from the waiting list */
+
+static void mock_unix_server_unlink(MOCK_SERVER *mp)
+{
+    if (mp->next)
+       mp->next->prev = mp->prev;
+    if (mp->prev)
+       mp->prev->next = mp->next;
+    mp->prev = mp->next = 0;
+}
+
+/* mock_server_free - destroy mock unix-domain server */
+
+void    mock_server_free(MOCK_SERVER *mp)
+{
+    const char myname[] = "mock_server_free";
+
+    myfree(mp->want_dest);
+    if ((mp->flags & MOCK_SERVER_FLAG_CONNECTED) == 0)
+       (void) close(mp->fds[MOCK_CLIENT_SIDE]);
+    MOCK_SERVER_CLEAR_EVENT_REQUEST(mp->fds[MOCK_SERVER_SIDE],
+                                   mock_unix_server_read_event, mp);
+    (void) close(mp->fds[MOCK_SERVER_SIDE]);
+    if (mp->want_req)
+       vstring_free(mp->want_req);
+    if (mp->resp)
+       vstring_free(mp->resp);
+    if (mp->iobuf)
+       vstring_free(mp->iobuf);
+    mock_unix_server_unlink(mp);
+    myfree(mp);
+}
+
+/* mock_server_free_void_ptr - destroy mock unix-domain server */
+
+void    mock_server_free_void_ptr(void *ptr)
+{
+    mock_server_free(ptr);
+}
+
+/* unix_connect - mock helper */
+
+int     unix_connect(const char *dest, int block_mode, int unused_timeout)
+{
+    MOCK_SERVER *mp;
+
+    for (mp = mock_unix_server_head.next; /* see below */ ; mp = mp->next) {
+       if (mp == 0) {
+           errno = ENOENT;
+           return (-1);
+       }
+       if (strcmp(dest, mp->want_dest) == 0) {
+           mock_unix_server_unlink(mp);
+           if (block_mode == NON_BLOCKING)
+               non_blocking(mp->fds[MOCK_CLIENT_SIDE], block_mode);
+           mp->flags |= MOCK_SERVER_FLAG_CONNECTED;
+           return (mp->fds[MOCK_CLIENT_SIDE]);
+       }
+    }
+}
diff --git a/postfix/src/testing/mock_server.h b/postfix/src/testing/mock_server.h
new file mode 100644 (file)
index 0000000..c97d634
--- /dev/null
@@ -0,0 +1,53 @@
+#ifndef _MOCK_SERVER_H_INCLUDED_
+#define _MOCK_SERVER_H_INCLUDED_
+
+/*++
+/* NAME
+/*     mock_server 3h
+/* SUMMARY
+/*     Mock server support
+/* SYNOPSIS
+/*     #include <mock_server.h>
+/* DESCRIPTION
+/* .nf
+
+ /*
+  * Utility library.
+  */
+#include <vstring.h>
+#include <connect.h>
+
+ /*
+  * External interface.
+  */
+typedef struct MOCK_SERVER {
+    int     flags;
+    int     fds[2];                    /* pipe(2) result */
+    char   *want_dest;
+    VSTRING *want_req;                 /* serialized request, may be null */
+    VSTRING *resp;                     /* serialized response, may be null */
+    VSTRING *iobuf;                    /* I/O buffer */
+    struct MOCK_SERVER *next;  /* chain of unconnected servers */
+    struct MOCK_SERVER *prev;  /* chain of unconnected servers */
+} MOCK_SERVER;
+
+#define MOCK_SERVER_FLAG_CONNECTED     (1<<0)
+
+extern MOCK_SERVER *mock_unix_server_create(const char *);
+extern void mock_server_interact(MOCK_SERVER *, const VSTRING *,
+                                             const VSTRING *);
+extern void mock_server_free(MOCK_SERVER *);
+extern void mock_server_free_void_ptr(void *);
+
+/* LICENSE
+/* .ad
+/* .fi
+/*     The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/*     Wietse Venema
+/*     Google, Inc.
+/*     111 8th Avenue
+/*     New York, NY 10011, USA
+/*--*/
+
+#endif
diff --git a/postfix/src/testing/mock_server_test.c b/postfix/src/testing/mock_server_test.c
new file mode 100644 (file)
index 0000000..4dd8ee9
--- /dev/null
@@ -0,0 +1,426 @@
+ /*
+  * Test program to exercise mock_server.c. See PTEST_README for
+  * documentation for how this file is structured.
+  */
+
+ /*
+  * System library.
+  */
+#include <sys_defs.h>
+#include <errno.h>
+#include <stdarg.h>
+
+ /*
+  * Utility library.
+  */
+#include <attr.h>
+#include <events.h>
+#include <msg.h>
+#include <vstream.h>
+#include <vstring.h>
+
+ /*
+  * Global library.
+  */
+#include <mail_proto.h>
+
+ /*
+  * Test library.
+  */
+#include <make_attr.h>
+#include <mock_server.h>
+#include <ptest.h>
+
+ /*
+  * Generic case structure.
+  */
+typedef struct PTEST_CASE {
+    const char *testname;              /* Human-readable description */
+    void    (*action) (PTEST_CTX *t, const struct PTEST_CASE *);
+} PTEST_CASE;
+
+ /*
+  * Structure to capture a client-server conversation state.
+  */
+struct session_state {
+    VSTRING *resp_buf;                 /* request echoed by server */
+    int     resp_len;                  /* request length from server */
+    int     fd;
+    VSTREAM *stream;
+    int     error;
+};
+
+#define REQUEST_READ_EVENT(fd, action, context, timeout) do { \
+        if (msg_verbose > 1) \
+            msg_info("%s: read-request fd=%d", myname, (fd)); \
+        event_enable_read((fd), (action), (context)); \
+        event_request_timer((action), (context), (timeout)); \
+    } while (0)
+
+#define CLEAR_EVENT_REQUEST(fd, time_act, context) do { \
+        if (msg_verbose > 1) \
+            msg_info("%s: clear-request fd=%d", myname, (fd)); \
+        event_disable_readwrite(fd); \
+        event_cancel_timer((time_act), (context)); \
+    } while (0)
+
+/* read_event - event handler to receive server response */
+
+static void read_event(int event, void *context)
+{
+    static const char myname[] = "read_event";
+    struct session_state *session_state = (struct session_state *) context;
+
+    CLEAR_EVENT_REQUEST(session_state->fd, read_event, context);
+
+    switch (event) {
+    case EVENT_READ:
+       if (attr_scan(session_state->stream, ATTR_FLAG_NONE,
+                     RECV_ATTR_STR(MAIL_ATTR_REQ, session_state->resp_buf),
+                   RECV_ATTR_INT(MAIL_ATTR_SIZE, &session_state->resp_len),
+                     ATTR_TYPE_END) != 2) {
+           ptest_error(ptest_ctx_current(), "%s failed: %m", myname);
+           session_state->error = EINVAL;
+       }
+       break;
+    case EVENT_TIME:
+       ptest_error(ptest_ctx_current(), "%s: timeout", myname);
+       session_state->error = ETIMEDOUT;
+       break;
+    default:
+       ptest_fatal(ptest_ctx_current(), "%s: unknown event: %d",
+                   myname, event);
+    }
+}
+
+static void test_single_server(PTEST_CTX *t, const PTEST_CASE *tp)
+{
+    static const char myname[] = "test_single_server";
+    MOCK_SERVER *mp;
+    struct session_state session_state;
+    VSTRING *serialized_req;
+    VSTRING *serialized_resp;
+
+#define REQUEST_VAL    "abcdef"
+#define SERVER_NAME    "testing..."
+
+    /*
+     * Instantiate a mock server, and connect to it.
+     */
+    mp = mock_unix_server_create(SERVER_NAME);
+    session_state.resp_buf = vstring_alloc(100);
+    session_state.resp_len = 0;
+    session_state.error = 0;
+    if ((session_state.fd = unix_connect(SERVER_NAME, 0, 0)) < 0)
+       ptest_fatal(t, "unix_connect: %s: %m", SERVER_NAME);
+    session_state.stream = vstream_fdopen(session_state.fd, O_RDWR);
+
+    /*
+     * Set up a server request expectation, and response.
+     */
+    serialized_req =
+       make_attr(ATTR_FLAG_NONE,
+                 SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL),
+                 ATTR_TYPE_END);
+    serialized_resp =
+       make_attr(ATTR_FLAG_NONE,
+                 SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL),
+                 SEND_ATTR_INT(MAIL_ATTR_SIZE, strlen(REQUEST_VAL)),
+                 ATTR_TYPE_END);
+    mock_server_interact(mp, serialized_req, serialized_resp);
+
+    /*
+     * Send a request, and run the event loop once to notify the server side
+     * that the request is pending.
+     */
+    if (attr_print(session_state.stream, ATTR_FLAG_NONE,
+                  SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL),
+                  ATTR_TYPE_END) != 0
+       || vstream_fflush(session_state.stream) != 0)
+       ptest_fatal(t, "send request: %m");
+    event_loop(1);
+
+    /*
+     * Receive the response, and validate.
+     */
+    REQUEST_READ_EVENT(session_state.fd, read_event, &session_state, 1);
+    event_loop(1);
+    if (session_state.error != 0) {
+       /* already reported */
+    } else if (VSTRING_LEN(session_state.resp_buf) != strlen(REQUEST_VAL)) {
+       ptest_error(t, "got resp_buf length %ld, want %ld",
+                   (long) VSTRING_LEN(session_state.resp_buf),
+                   (long) strlen(REQUEST_VAL));
+    } else if (session_state.resp_len != strlen(REQUEST_VAL)) {
+       ptest_error(t, "got resp_len %d, want %ld",
+                   session_state.resp_len, (long) strlen(REQUEST_VAL));
+    } else if (strcmp(vstring_str(session_state.resp_buf), REQUEST_VAL) != 0) {
+       ptest_error(t, "got resp_buf '%s', wamt '%s'",
+                   vstring_str(session_state.resp_buf), REQUEST_VAL);
+    }
+
+    /*
+     * Clean up.
+     */
+    if (vstream_fclose(session_state.stream) != 0)
+       ptest_fatal(t, "close stream: %m");
+    vstring_free(session_state.resp_buf);
+    vstring_free(serialized_req);
+    vstring_free(serialized_resp);
+    mock_server_free(mp);
+}
+
+static void test_request_mismatch(PTEST_CTX *t, const PTEST_CASE *tp)
+{
+    static const char myname[] = "test_single_server";
+    MOCK_SERVER *mp;
+    struct session_state session_state;
+    VSTRING *serialized_req;
+    VSTRING *serialized_resp;
+
+#define REQUEST_VAL    "abcdef"
+#define SERVER_NAME    "testing..."
+
+    /*
+     * Instantiate a mock server, and connect to it.
+     */
+    mp = mock_unix_server_create(SERVER_NAME);
+    session_state.resp_buf = vstring_alloc(100);
+    session_state.resp_len = 0;
+    if ((session_state.fd = unix_connect(SERVER_NAME, 0, 0)) < 0)
+       ptest_fatal(t, "unix_connect: %s: %m", SERVER_NAME);
+    session_state.stream = vstream_fdopen(session_state.fd, O_RDWR);
+
+    /*
+     * Set up a server request expectation, and response.
+     */
+    serialized_req =
+       make_attr(ATTR_FLAG_NONE,
+                 SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL "g"),
+                 ATTR_TYPE_END);
+    serialized_resp =
+       make_attr(ATTR_FLAG_NONE,
+                 SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL),
+                 SEND_ATTR_INT(MAIL_ATTR_SIZE, strlen(REQUEST_VAL)),
+                 ATTR_TYPE_END);
+    mock_server_interact(mp, serialized_req, serialized_resp);
+
+    /*
+     * Send a request, and run the event loop once to notify the server side
+     * that the request is pending.
+     */
+    if (attr_print(session_state.stream, ATTR_FLAG_NONE,
+                  SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL),
+                  ATTR_TYPE_END) != 0
+       || vstream_fflush(session_state.stream) != 0)
+       ptest_fatal(t, "send request: %m");
+    expect_ptest_error(t, "attributes differ");
+    expect_ptest_error(t, "+request = abcdef");
+    expect_ptest_error(t, "-request = abcdefg");
+    expect_ptest_error(t, "timeout");
+    event_loop(1);
+
+    /*
+     * Receive the response, and validate.
+     */
+    REQUEST_READ_EVENT(session_state.fd, read_event, &session_state, 1);
+    event_loop(1);
+    if (session_state.error != 0) {
+        /* already reported */
+    } else if (VSTRING_LEN(session_state.resp_buf) != strlen(REQUEST_VAL)) {
+        ptest_error(t, "got resp_buf length %ld, want %ld",
+                    (long) VSTRING_LEN(session_state.resp_buf),
+                    (long) strlen(REQUEST_VAL));
+    } else if (session_state.resp_len != strlen(REQUEST_VAL)) {
+        ptest_error(t, "got resp_len %d, want %ld",
+                    session_state.resp_len, (long) strlen(REQUEST_VAL));
+    } else if (strcmp(vstring_str(session_state.resp_buf), REQUEST_VAL) != 0) {
+        ptest_error(t, "got resp_buf '%s', wamt '%s'",
+                    vstring_str(session_state.resp_buf), REQUEST_VAL);
+    }
+
+    /*
+     * Clean up.
+     */
+    if (vstream_fclose(session_state.stream) != 0)
+       ptest_fatal(t, "close stream: %m");
+    vstring_free(session_state.resp_buf);
+    vstring_free(serialized_req);
+    vstring_free(serialized_resp);
+    mock_server_free(mp);
+}
+
+static void test_missing_server(PTEST_CTX *t, const PTEST_CASE *tp)
+{
+    int     fd;
+
+#define SERVER_NAME "testing..."
+
+    /*
+     * Connect to a non-existent server, and require a failure.
+     */
+    if ((fd = unix_connect(SERVER_NAME, 0, 0)) >= 0) {
+       (void) close(fd);
+       ptest_fatal(t,
+                   "unix_connect(%s) did NOT fail", SERVER_NAME);
+    }
+}
+
+static void test_unused_server(PTEST_CTX *t, const PTEST_CASE *tp)
+{
+    MOCK_SERVER *mp;
+
+    /*
+     * Instantiate a mock server, and destroy it without using it.
+     */
+    mp = mock_unix_server_create(SERVER_NAME);
+    mock_server_free(mp);
+}
+
+static void test_server_speaks_only(PTEST_CTX *t, const PTEST_CASE *tp)
+{
+    static const char myname[] = "test_server_speaks_only";
+    MOCK_SERVER *mp;
+    struct session_state session_state;
+    VSTRING *serialized_resp;
+
+    /*
+     * This is the same test as "test_single_server", but without sending a
+     * request.
+     */
+#define REQUEST_VAL    "abcdef"
+#define SERVER_NAME    "testing..."
+#define NO_REQUEST     ((VSTRING *) 0)
+
+    /*
+     * Instantiate a mock server, and connect to it.
+     */
+    mp = mock_unix_server_create(SERVER_NAME);
+    session_state.resp_buf = vstring_alloc(100);
+    session_state.resp_len = 0;
+    if ((session_state.fd = unix_connect(SERVER_NAME, 0, 0)) < 0)
+       ptest_fatal(t, "unix_connect: %s: %m", SERVER_NAME);
+    session_state.stream = vstream_fdopen(session_state.fd, O_RDWR);
+
+    /*
+     * Set up a server response, without request expectation.
+     */
+    serialized_resp =
+       make_attr(ATTR_FLAG_NONE,
+                 SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL),
+                 SEND_ATTR_INT(MAIL_ATTR_SIZE, strlen(REQUEST_VAL)),
+                 ATTR_TYPE_END);
+    mock_server_interact(mp, NO_REQUEST, serialized_resp);
+
+    /*
+     * Receive the response, and validate.
+     */
+    REQUEST_READ_EVENT(session_state.fd, read_event, &session_state, 1);
+    event_loop(1);
+    if (session_state.error != 0) {
+        /* already reported */
+    } else if (VSTRING_LEN(session_state.resp_buf) != strlen(REQUEST_VAL)) {
+        ptest_error(t, "got resp_buf length %ld, want %ld",
+                    (long) VSTRING_LEN(session_state.resp_buf),
+                    (long) strlen(REQUEST_VAL));
+    } else if (session_state.resp_len != strlen(REQUEST_VAL)) {
+        ptest_error(t, "got resp_len %d, want %ld",
+                    session_state.resp_len, (long) strlen(REQUEST_VAL));
+    } else if (strcmp(vstring_str(session_state.resp_buf), REQUEST_VAL) != 0) {
+        ptest_error(t, "got resp_buf '%s', wamt '%s'",
+                    vstring_str(session_state.resp_buf), REQUEST_VAL);
+    }
+
+    /*
+     * Clean up.
+     */
+    if (vstream_fclose(session_state.stream) != 0)
+       ptest_fatal(t, "close stream: %m");
+    vstring_free(session_state.resp_buf);
+    vstring_free(serialized_resp);
+    mock_server_free(mp);
+}
+
+static void test_client_speaks_only(PTEST_CTX *t, const PTEST_CASE *tp)
+{
+    MOCK_SERVER *mp;
+    struct session_state session_state;
+    VSTRING *serialized_req;
+
+    /*
+     * This is the same test as "test_single_server", but without receiving a
+     * response.
+     */
+#define REQUEST_VAL    "abcdef"
+#define SERVER_NAME    "testing..."
+#define NO_RESPONSE    ((VSTRING *) 0)
+
+    /*
+     * Instantiate a mock server, and connect to it.
+     */
+    mp = mock_unix_server_create(SERVER_NAME);
+    if ((session_state.fd = unix_connect(SERVER_NAME, 0, 0)) < 0)
+       ptest_fatal(t, "unix_connect: %s: %m", SERVER_NAME);
+    session_state.stream = vstream_fdopen(session_state.fd, O_RDWR);
+
+    /*
+     * Set up a server request expectation, and response.
+     */
+    serialized_req =
+       make_attr(ATTR_FLAG_NONE,
+                 SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL),
+                 ATTR_TYPE_END);
+    mock_server_interact(mp, serialized_req, NO_RESPONSE);
+
+    /*
+     * Send a request, and run the event loop once to notify the server side
+     * that the request is pending.
+     */
+    if (attr_print(session_state.stream, ATTR_FLAG_NONE,
+                  SEND_ATTR_STR(MAIL_ATTR_REQ, REQUEST_VAL),
+                  ATTR_TYPE_END) != 0
+       || vstream_fflush(session_state.stream) != 0)
+       ptest_fatal(t, "send request: %m");
+    event_loop(1);
+
+    /*
+     * Clean up.
+     */
+    if (vstream_fclose(session_state.stream) != 0)
+       ptest_fatal(t, "close stream: %m");
+    vstring_free(serialized_req);
+    mock_server_free(mp);
+}
+
+ /*
+  * Test cases.
+  */
+const PTEST_CASE ptestcases[] = {
+    {
+       "test single server", test_single_server,
+    },
+    {
+       "test request mismatch", test_request_mismatch,
+    },
+    {
+       "test missing server", test_missing_server,
+    },
+    {
+       "test unused server", test_unused_server,
+    },
+    {
+       "test server speaks only", test_server_speaks_only,
+    },
+    {
+       "test client speaks only", test_client_speaks_only,
+    },
+
+    /*
+     * TODO: test multiple servers with the same endpoint name but with
+     * different expectations. See postscreen_dnsbl_test.c for an example.
+     * This requires that the environment variable "NORAMDOMIZE" is set
+     * before this program is run.
+     */
+};
+
+#include <ptest_main.h>
index d604962f32427fc76cffd7e652509b330fc66796..95218ca604c21aa6a1efa228c156b14436da597c 100644 (file)
@@ -1,6 +1,5 @@
  /*
-  * Test program to exercise argv.c. See ptest_main.h for a documented
-  * example.
+  * Test program to exercise argv.c. See PTEST_README for documentation.
   */
 
  /*
index 8ce0faad728d5f973fc2718bbe2917b6670997a5..146e837a22e110295444e45e64fda98b4b7116cd 100644 (file)
 /*     IBM T.J. Watson Research
 /*     P.O. Box 704
 /*     Yorktown Heights, NY 10598, USA
+/*
+/*     Wietse Venema
+/*     Google, Inc.
+/*     111 8th Avenue
+/*     New York, NY 10011, USA
 /*--*/
 
 /* System library. */
index 4a566ef338058a45eef9511f528f76b2aff0ec02..d56dffd2cab30ca8fd6fc2c21113a5dfbe70447d 100644 (file)
@@ -1,6 +1,5 @@
  /*
-  * Test program to exercise dict_pipe.c. See ptest_main.h for a documented
-  * example.
+  * Test program to exercise dict_pipe.c. See PTEST_README for documentation. 
   */
 
  /*
index 47332b422f66f2576e6528243aded7f0467f71e3..5cbb54870d6ddd82eae2866b3e50b4476be557d4 100644 (file)
@@ -1,6 +1,6 @@
  /*
-  * Test program to exercise dict_stream.c. See ptest_main.h for a documented
-  * example.
+  * Test program to exercise dict_stream.c. See PTEST_README for
+  * documentation.
   */
 
  /*
index 845dc9b3103186f220d0f5fad8929827c9706c9b..83b4e8ae9cb1d776524a0948cc78ea1bfc56c300 100644 (file)
@@ -1,6 +1,6 @@
  /*
-  * Test program to exercise dict_union.c. See ptest_main.h for a documented
-  * example.
+  * Test program to exercise dict_union.c. See PTEST_README for
+  * documentation.
   */
 
  /*
@@ -77,7 +77,7 @@ static void test_dict_union(PTEST_CTX *t, const struct PTEST_CASE *tp)
 
 static const PTEST_CASE ptestcases[] = {
     {
-        /* name */ "successful lookup: static map + inline map",
+        /* testname */ "successful lookup: static map + inline map",
         /* action */ test_dict_union,
         /* type_name */ "unionmap:{static:one,inline:{foo=two}}",
         /* probes */ {
@@ -85,14 +85,14 @@ static const PTEST_CASE ptestcases[] = {
            {"bar", "one"},
        },
     }, {
-        /* name */ "error propagation: static map + fail map",
+        /* testname */ "error propagation: static map + fail map",
         /* action */ test_dict_union,
         /* type_name */ "unionmap:{static:one,fail:fail}",
         /* probes */ {
            {"foo", 0, DICT_STAT_ERROR},
        },
     }, {
-        /* name */ "error propagation: fail map + static map",
+        /* testname */ "error propagation: fail map + static map",
         /* action */ test_dict_union,
         /* type_name */ "unionmap:{fail:fail,static:one}",
         /* probes */ {
index 11b60b39cf5bf5ce194bfaa47e3986c95e882888..ba806e80c6b79e1843d274bb305e06bcbe4a02bb 100644 (file)
@@ -1,6 +1,6 @@
  /*
   * Test program to exercise find_inet_service.c. See pmock_expect_test.c and
-  * ptest_main.h for a documented example.
+  * PTEST_README for documentation.
   */
 
  /*
index 6255a1d81c99c00cb81ebecd375e9a22b2c96d0e..a2f85eddb07259c7a1b2e424b698a8781b92fbc4 100644 (file)
@@ -1,6 +1,6 @@
  /*
-  * Test program to exercise the hash_fnv implementation. See comments in
-  * ptest_main.h for a documented example.
+  * Test program to exercise the hash_fnv implementation. See PTEST_README
+  * for documentation.
   */
 
  /*
index bdd1084e8dc89940880126bf721b23c15245e929..2655de8e394503c5ed690c92f03234803ce47a11 100644 (file)
@@ -1,6 +1,6 @@
  /*
-  * Test program to exercise known_tcp_ports.c. See ptest_main.h for a
-  * documented example.
+  * Test program to exercise known_tcp_ports.c. See PTEST_README for
+  * documentation.
   */
 
  /*
index 48c6402d895af8b49629fec76db2ad07f15c937a..cc7cb1dd739642f0980fd9116c38e4f146579eb8 100644 (file)
@@ -1,6 +1,6 @@
  /*
-  * Test program to exercise the msg_output module. See comments in
-  * ptest_main.h for documented examples.
+  * Test program to exercise the msg_output module. See PTEST_README for
+  * documentation.
   */
 
  /*
index 4a8cf0f7a8a315090d4797f8eee196d8efb13b29..d334ee78da005c244fba774f3efc064d358fad00 100644 (file)
@@ -1,8 +1,8 @@
  /*
   * Test program for the myaddrinfo module. The purpose is to verify that the
   * myaddrinfo functions make the expected calls, and that they forward the
-  * expected results. See comments in ptest_main.h and pmock_expect_test.c
-  * for a documented example.
+  * expected results. See comments in pmock_expect_test.c, and PTEST_README
+  * for documentation.
   */
 
  /*
index fc3c8c036ba3853ce32689aba9384508b7d7383b..a87743e60acb984c310b7150d217aa57eb59a4c8 100644 (file)
@@ -1,7 +1,6 @@
  /*
-  * Tests to verify malloc sanity checks. See comments in ptest_main.h for a
-  * documented example. The test code depends on the real mymalloc library,
-  * so we can 't do destructive tests.
+  * Tests to verify mymalloc sanity checks. See PTEST_README for
+  * documentation.
   */
 
  /*
@@ -221,7 +220,7 @@ static void test_mystrndup_static_empty(PTEST_CTX *t,
                    got, want);
 
     /*
-     * myfree() is a NOOP.
+     * myfree() is a NOOP for "empty" mystrdup() or mystrndup() results.
      */
     myfree(want);
     myfree(got);
@@ -258,7 +257,7 @@ static void test_mymemdup_fatal_out_of_mem(PTEST_CTX *t, const PTEST_CASE *tp)
     ptest_fatal(t, "mymemdup(_, SSIZE_T_MAX-100) returned");
 }
 
-const PTEST_CASE ptestcases[] = {
+static const PTEST_CASE ptestcases[] = {
     {"mymalloc + myfree normal case", test_mymalloc_normal,
     },
     {"mymalloc panic for too small request", test_mymalloc_panic_too_small,
index 9cc50a4a2856735feaf2e0f1fd06f1a11421d354..7f5800f95ee6abbd57a6448412a44d53f038df12 100644 (file)
@@ -1,6 +1,5 @@
  /*
-  * Test program to exercise mystrtok.c. See ptest_main.h for a documented
-  * example.
+  * Test program to exercise mystrtok.c. See PTEST_README for documentation.
   */
 
  /*
index 4eacbba608e42648d8ac35543ee64b3e2792774c..1e7519f898ae77653659b81e105160c2f2e86fea 100644 (file)
 /*     IBM T.J. Watson Research
 /*     P.O. Box 704
 /*     Yorktown Heights, NY 10598, USA
+/*
+/*     Wietse Venema
+/*     Google, Inc.
+/*     111 8th Avenue
+/*     New York, NY 10011, USA
 /*--*/
 
 /* System library. */
index 9a22749a923cc7411b67f0329fb4910916ab9936..75fe523fd526d7dc05b1b041a9c71f8fdeb0e3e4 100644 (file)
@@ -1,6 +1,5 @@
  /*
-  * Test program to exercise unescape.c. See ptest_main.h for a documented
-  * example.
+  * Test program to exercise unescape.c. See PTEST_README for documentation.
   */
 
  /*