-TABOUNCE_STATE
--Taddrinfo
-TADDR_MATCH_LIST
-TADDR_PATTERN
-TALIAS_TOKEN
-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
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:
* 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
+
--- /dev/null
+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.
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
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:
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.
+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,
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.
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?
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).
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.
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.
# 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
# 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
# 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)
$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
$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
--- /dev/null
+<!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 <ptest_main.h>
+</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 <ptest_main.h></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 <ptest_main.h>
+</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 <ptest_main.h>
+</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
+<what you got>, want <what you want>". </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>
+
<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>
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>
<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:
<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.
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
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>
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>
</ul>
+<p><strong> For maintainers and contributors </strong></p>
+
+<ul>
+
+<li> <a href="PTEST_README.html"> Writing Postfix unit tests </a>
+
+</ul>
+
</td>
</table>
<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>
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>
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
.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
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:
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/;
../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 \
../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 \
../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) >$@
../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) >$@
--- /dev/null
+<!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 <ptest_main.h>
+</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 <ptest_main.h></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 <ptest_main.h>
+</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 <ptest_main.h>
+</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
+<what you got>, want <what you want>". </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>
+
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>
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>
<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:
# 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
%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>
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>
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
ptestcases
subtests
case's
+HAPROXY
+SRVR
+Deserialize
+SNDBUF
+deserialize
+deserialized
+NORAMDOMIZE
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
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 \
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) \
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
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
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
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
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
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
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
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
/* 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)")
}
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
#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
--- /dev/null
+ /*
+ * 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>
#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"
#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.
* 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
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);
escape_order(want_escaped,
string_or_null(tp->exp_search_order)));
}
-/**INDENT** Warning@168: Extra ) */
});
}
vstring_free(want_escaped);
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) \
test: $(TESTPROG)
-tests: test
+tests: test_postscreen_dnsbl
root_tests:
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 \
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
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
/*
/* 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
/* 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
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) */
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.
*
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; \
} 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)
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
vstream_fclose(stream);
}
+static int request_count;
+
/* psc_dnsbl_request - send dnsbl query, increment reference count */
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,
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;
+ }
}
--- /dev/null
+ /*
+ * 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>
t = parent; \
} while (0)
+ /*
+ * How many elements in a test case array.
+ */
+#define PTEST_NROF(x) (sizeof(x)/sizeof((x)[0]))
+
/* LICENSE
/* .ad
/* .fi
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.
*/
* 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, {
/*
/* 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.
/*
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
#include <attr_override.h>
#include <map_search.h>
#include <info_log_addr_form.h>
+#include <mail_version.h>
/* Application-specific. */
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),
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) \
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)
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 \
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
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
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
--- /dev/null
+/*++
+/* 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);
+}
--- /dev/null
+#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
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;
* Test cases.
*/
const PTEST_CASE ptestcases[] = {
+ {
+ "Compare equal IPv4 addrinfos", test_eq_addrinfo_equal,
+ },
{
"Compare different IPv4 addrinfos", test_eq_addrinfo_diff,
},
--- /dev/null
+/*++
+/* 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);
+}
--- /dev/null
+#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
--- /dev/null
+/*
+ * 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>
--- /dev/null
+/*++
+/* 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]);
+ }
+ }
+}
--- /dev/null
+#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
--- /dev/null
+ /*
+ * 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>
/*
- * 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.
*/
/*
/* 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. */
/*
- * 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.
*/
/*
/*
- * 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.
*/
/*
/*
- * 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.
*/
/*
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 */ {
{"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 */ {
/*
* 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.
*/
/*
/*
- * 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.
*/
/*
/*
- * 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.
*/
/*
/*
- * 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.
*/
/*
/*
* 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.
*/
/*
/*
- * 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.
*/
/*
got, want);
/*
- * myfree() is a NOOP.
+ * myfree() is a NOOP for "empty" mystrdup() or mystrndup() results.
*/
myfree(want);
myfree(got);
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,
/*
- * 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.
*/
/*
/* 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. */
/*
- * 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.
*/
/*