]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
[master] expanded libns unit tests
authorMichał Kępień <michal@isc.org>
Wed, 11 Oct 2017 22:02:50 +0000 (15:02 -0700)
committerMichał Kępień <michal@isc.org>
Wed, 11 Oct 2017 22:02:50 +0000 (15:02 -0700)
4772. [test] Expanded unit testing framework for libns, using
hooks to interrupt query flow and inspect state
at specified locations. [RT #46173]

12 files changed:
CHANGES
configure
configure.in
lib/ns/hooks.h [new file with mode: 0644]
lib/ns/include/ns/query.h
lib/ns/query.c
lib/ns/tests/Makefile.in
lib/ns/tests/notify_test.c
lib/ns/tests/nstest.c
lib/ns/tests/nstest.h
lib/ns/tests/query_test.c [new file with mode: 0644]
lib/ns/tests/testdata/query/foo.db [new file with mode: 0644]

diff --git a/CHANGES b/CHANGES
index 0a334814cd4d2eb580bfcbb6aa915123403070da..92ef0bc9907291c1fbecd0dbca23b68646b53a90 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,7 @@
+4772.  [test]          Expanded unit testing framework for libns, using
+                       hooks to interrupt query flow and inspect state
+                       at specified locations. [RT #46173]
+
 4771.  [bug]           When sending RFC 5011 refresh queries, disregard
                        cached DNSKEY rrsets. [RT #46251]
 
index 4e1ee1d5b8656fda836c27a85b82cd8426f88709..7b66f9b9a4d29b3f1f69e92ec9611627b02543c6 100755 (executable)
--- a/configure
+++ b/configure
@@ -940,6 +940,7 @@ infodir
 docdir
 oldincludedir
 includedir
+runstatedir
 localstatedir
 sharedstatedir
 sysconfdir
@@ -1103,6 +1104,7 @@ datadir='${datarootdir}'
 sysconfdir='${prefix}/etc'
 sharedstatedir='${prefix}/com'
 localstatedir='${prefix}/var'
+runstatedir='${localstatedir}/run'
 includedir='${prefix}/include'
 oldincludedir='/usr/include'
 docdir='${datarootdir}/doc/${PACKAGE_TARNAME}'
@@ -1355,6 +1357,15 @@ do
   | -silent | --silent | --silen | --sile | --sil)
     silent=yes ;;
 
+  -runstatedir | --runstatedir | --runstatedi | --runstated \
+  | --runstate | --runstat | --runsta | --runst | --runs \
+  | --run | --ru | --r)
+    ac_prev=runstatedir ;;
+  -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \
+  | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \
+  | --run=* | --ru=* | --r=*)
+    runstatedir=$ac_optarg ;;
+
   -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb)
     ac_prev=sbindir ;;
   -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \
@@ -1492,7 +1503,7 @@ fi
 for ac_var in  exec_prefix prefix bindir sbindir libexecdir datarootdir \
                datadir sysconfdir sharedstatedir localstatedir includedir \
                oldincludedir docdir infodir htmldir dvidir pdfdir psdir \
-               libdir localedir mandir
+               libdir localedir mandir runstatedir
 do
   eval ac_val=\$$ac_var
   # Remove trailing slashes.
@@ -1645,6 +1656,7 @@ Fine tuning of the installation directories:
   --sysconfdir=DIR        read-only single-machine data [PREFIX/etc]
   --sharedstatedir=DIR    modifiable architecture-independent data [PREFIX/com]
   --localstatedir=DIR     modifiable single-machine data [PREFIX/var]
+  --runstatedir=DIR       modifiable per-process data [LOCALSTATEDIR/run]
   --libdir=DIR            object code libraries [EPREFIX/lib]
   --includedir=DIR        C header files [PREFIX/include]
   --oldincludedir=DIR     C header files for non-gcc [/usr/include]
@@ -11466,7 +11478,7 @@ fi
 XTARGETS=
 case "$enable_developer" in
 yes)
-       STD_CDEFINES="$STD_CDEFINES -DISC_MEM_DEFAULTFILL=1 -DISC_LIST_CHECKINIT=1"
+       STD_CDEFINES="$STD_CDEFINES -DISC_MEM_DEFAULTFILL=1 -DISC_LIST_CHECKINIT=1 -DNS_HOOKS_ENABLE=1"
        test "${enable_fixed_rrset+set}" = set || enable_fixed_rrset=yes
        test "${enable_querytrace+set}" = set || enable_querytrace=yes
        test "${with_atf+set}" = set || with_atf=yes
@@ -22225,6 +22237,7 @@ if test "no" != "$atf"; then
 $as_echo "#define ATF_TEST 1" >>confdefs.h
 
        STD_CINCLUDES="$STD_CINCLUDES -I$atf/include"
+        STD_CDEFINES="$STD_CDEFINES -DNS_HOOKS_ENABLE=1"
        ATFBIN="$atf/bin"
        ATFLIBS="-L$atf/lib -latf-c"
        { $as_echo "$as_me:${as_lineno-$LINENO}: checking for exp in -lm" >&5
index 2bbaf0098821fcfec8a2a3eb3b3233136c6fdac8..11545190dbe873a334608f03a3cb481a3490f220 100644 (file)
@@ -62,7 +62,7 @@ AC_ARG_ENABLE(developer, [  --enable-developer      enable developer build setti
 XTARGETS=
 case "$enable_developer" in
 yes)
-       STD_CDEFINES="$STD_CDEFINES -DISC_MEM_DEFAULTFILL=1 -DISC_LIST_CHECKINIT=1"
+       STD_CDEFINES="$STD_CDEFINES -DISC_MEM_DEFAULTFILL=1 -DISC_LIST_CHECKINIT=1 -DNS_HOOKS_ENABLE=1"
        test "${enable_fixed_rrset+set}" = set || enable_fixed_rrset=yes
        test "${enable_querytrace+set}" = set || enable_querytrace=yes
        test "${with_atf+set}" = set || with_atf=yes
@@ -4771,6 +4771,7 @@ ATFLIBS=
 if test "no" != "$atf"; then
        AC_DEFINE(ATF_TEST, 1, [define if ATF unit tests are to be built.])
        STD_CINCLUDES="$STD_CINCLUDES -I$atf/include"
+        STD_CDEFINES="$STD_CDEFINES -DNS_HOOKS_ENABLE=1"
        ATFBIN="$atf/bin"
        ATFLIBS="-L$atf/lib -latf-c"
        AC_CHECK_LIB(m, exp, libm=yes, libm=no)
diff --git a/lib/ns/hooks.h b/lib/ns/hooks.h
new file mode 100644 (file)
index 0000000..c16cebb
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2017  Internet Systems Consortium, Inc. ("ISC")
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef NS_HOOKS_H
+#define NS_HOOKS_H 1
+
+#ifdef NS_HOOKS_ENABLE
+
+/*! \file */
+
+#include <isc/result.h>
+
+/*
+ * Hooks provide a way of running a callback function once a certain place in
+ * code is reached.  Current use is limited to libns unit tests and thus:
+ *
+ *   - hook-related types and macros are not placed in libns header files,
+ *   - hook-related code is compiled away unless --enable-developer is used,
+ *   - hook-related macro names are prefixed with "NS_".
+ *
+ * However, the implementation is pretty generic and could be repurposed for
+ * general use, e.g. as part of libisc, after some further customization.
+ *
+ * Hooks are created by inserting a macro into any function returning
+ * isc_result_t (NS_PROCESS_HOOK()) or void (NS_PROCESS_HOOK_VOID()).  Each
+ * hook has an identifier, which is an integer that is an index into the hook
+ * table.  In an attempt to keep things as simple as possible, current
+ * implementation:
+ *
+ *   - uses hook tables which are statically-sized arrays only allowing a
+ *     single callback to be invoked for each hook identifier,
+ *   - only supports replacing whole hook tables.
+ *
+ * Hook callbacks are functions which:
+ *
+ *   - return a boolean value; if ISC_TRUE is returned by the callback, the
+ *     function into which the hook is inserted will return at hook insertion
+ *     point; if ISC_FALSE is returned by the callback, execution of the
+ *     function into which the hook is inserted continues normally,
+ *
+ *   - accept three pointers as arguments:
+ *
+ *       - a pointer specified by the hook itself,
+ *       - a pointer specified upon inserting the callback into the hook table,
+ *       - a pointer to isc_result_t which will be returned by the function
+ *         into which the hook is inserted if the callback returns ISC_TRUE.
+ *
+ * In order for a hook callback to be called for a given hook, a pointer to
+ * that callback (along with an optional pointer to callback-specific data) has
+ * to be inserted into the hook table entry for that hook.
+ *
+ * Consider the following sample code:
+ *
+ * ----------------------------------------------------------------------------
+ * const ns_hook_t *foo_hook_table = NULL;
+ *
+ * isc_result_t
+ * foo_bar(void) {
+ *     int val = 42;
+ *     ...
+ *     NS_PROCESS_HOOK(foo_hook_table, FOO_EXTRACT_VAL, &val);
+ *     ...
+ *     printf("This message may not be printed due to use of hooks.");
+ *
+ *     return (ISC_R_SUCCESS);
+ * }
+ *
+ * isc_boolean_t
+ * cause_failure(void *hook_data, void *callback_data, isc_result_t *resultp) {
+ *     ...
+ *     *resultp = ISC_R_FAILURE;
+ *
+ *     return (ISC_TRUE);
+ * }
+ *
+ * void
+ * test_foo_bar(void) {
+ *     isc_boolean_t foo_bar_called = ISC_FALSE;
+ *     const ns_hook_t my_hooks[FOO_HOOKS_COUNT] = {
+ *         [FOO_EXTRACT_VAL] = {
+ *             .callback = cause_failure,
+ *             .callback_data = &foo_bar_called,
+ *         },
+ *     };
+ *
+ *     foo_hook_table = my_hooks;
+ *
+ *     foo_bar();
+ * }
+ * ----------------------------------------------------------------------------
+ *
+ * When test_foo_bar() is called, the hook table is first replaced.  Then
+ * foo_bar() gets invoked.  Once execution reaches the insertion point for hook
+ * FOO_EXTRACT_VAL, cause_failure() will be called with &val as hook_data and
+ * &foo_bar_called as callback_data.  It can do whatever it pleases with these
+ * two values.  Eventually, cause_failure() sets *resultp to ISC_R_FAILURE and
+ * returns ISC_TRUE, which causes foo_bar() to return ISC_R_FAILURE and never
+ * execute the printf() call below hook insertion point.
+ */
+
+enum {
+       NS_QUERY_SETUP_QCTX_INITIALIZED,
+       NS_QUERY_LOOKUP_BEGIN,
+       NS_QUERY_DONE_BEGIN,
+       NS_QUERY_HOOKS_COUNT
+};
+
+typedef isc_boolean_t
+(*ns_hook_cb_t)(void *hook_data, void *callback_data, isc_result_t *resultp);
+
+typedef struct ns_hook {
+       ns_hook_cb_t callback;
+       void *callback_data;
+} ns_hook_t;
+
+#define _NS_PROCESS_HOOK(table, id, data, ...)                         \
+       if (table != NULL) {                                            \
+               ns_hook_cb_t _callback = table[id].callback;            \
+               void *_callback_data = table[id].callback_data;         \
+               isc_result_t _result;                                   \
+                                                                       \
+               if (_callback != NULL &&                                \
+                   _callback(data, _callback_data, &_result)) {        \
+                       return __VA_ARGS__;                             \
+               }                                                       \
+       }
+
+#define NS_PROCESS_HOOK(table, id, data) \
+       _NS_PROCESS_HOOK(table, id, data, _result)
+
+#define NS_PROCESS_HOOK_VOID(table, id, data) \
+       _NS_PROCESS_HOOK(table, id, data)
+
+LIBNS_EXTERNAL_DATA extern const ns_hook_t *ns__hook_table;
+
+#endif /* NS_HOOKS_ENABLE */
+#endif /* NS_HOOKS_H */
index ce6d06fc774f5441403c2716668d51a2b4561ffa..63b15a756982e5e6bc6ad779c75a85a63fa6cc03 100644 (file)
@@ -16,6 +16,7 @@
 #include <isc/netaddr.h>
 
 #include <dns/rdataset.h>
+#include <dns/resolver.h>
 #include <dns/rpz.h>
 #include <dns/types.h>
 
@@ -92,6 +93,55 @@ struct ns_query {
 #define NS_QUERYATTR_RRL_CHECKED       0x10000
 #define NS_QUERYATTR_REDIRECT          0x20000
 
+/* query context structure */
+
+typedef struct query_ctx {
+       isc_buffer_t *dbuf;                     /* name buffer */
+       dns_name_t *fname;                      /* found name from DB lookup */
+       dns_rdataset_t *rdataset;               /* found rdataset */
+       dns_rdataset_t *sigrdataset;            /* found sigrdataset */
+       dns_rdataset_t *noqname;                /* rdataset needing
+                                                * NOQNAME proof */
+       dns_rdatatype_t qtype;
+       dns_rdatatype_t type;
+
+       unsigned int options;                   /* DB lookup options */
+
+       isc_boolean_t redirected;               /* nxdomain redirected? */
+       isc_boolean_t is_zone;                  /* is DB a zone DB? */
+       isc_boolean_t is_staticstub_zone;
+       isc_boolean_t resuming;                 /* resumed from recursion? */
+       isc_boolean_t dns64, dns64_exclude, rpz;
+       isc_boolean_t authoritative;            /* authoritative query? */
+       isc_boolean_t want_restart;             /* CNAME chain or other
+                                                * restart needed */
+       isc_boolean_t need_wildcardproof;       /* wilcard proof needed */
+       isc_boolean_t nxrewrite;                /* negative answer from RPZ */
+       isc_boolean_t findcoveringnsec;         /* lookup covering NSEC */
+       isc_boolean_t want_stale;               /* want stale records? */
+       dns_fixedname_t wildcardname;           /* name needing wcard proof */
+       dns_fixedname_t dsname;                 /* name needing DS */
+
+       ns_client_t *client;                    /* client object */
+       dns_fetchevent_t *event;                /* recursion event */
+
+       dns_db_t *db;                           /* zone or cache database */
+       dns_dbversion_t *version;               /* DB version */
+       dns_dbnode_t *node;                     /* DB node */
+
+       dns_db_t *zdb;                          /* zone DB values, saved */
+       dns_name_t *zfname;                     /* while searching cache */
+       dns_dbversion_t *zversion;              /* for a better answer */
+       dns_rdataset_t *zrdataset;
+       dns_rdataset_t *zsigrdataset;
+
+       dns_rpz_st_t *rpz_st;                   /* RPZ state */
+       dns_zone_t *zone;                       /* zone to search */
+
+       isc_result_t result;                    /* query result */
+       int line;                               /* line to report error */
+} query_ctx_t;
+
 isc_result_t
 ns_query_init(ns_client_t *client);
 
@@ -104,4 +154,16 @@ ns_query_start(ns_client_t *client);
 void
 ns_query_cancel(ns_client_t *client);
 
+/*%
+ * (Must not be used outside this module and its associated unit tests.)
+ */
+isc_result_t
+ns__query_sfcache(query_ctx_t *qctx);
+
+/*%
+ * (Must not be used outside this module and its associated unit tests.)
+ */
+isc_result_t
+ns__query_start(query_ctx_t *qctx);
+
 #endif /* NS_QUERY_H */
index 7e19ff06fc12287d0f4212a2805f8091f0243b9e..8c9fa5c0c80eb480d68189b477aca970b2f122c2 100644 (file)
@@ -62,6 +62,8 @@
 #include <ns/stats.h>
 #include <ns/xfrout.h>
 
+#include "hooks.h"
+
 #if 0
 /*
  * It has been recommended that DNS64 be changed to return excluded
@@ -243,17 +245,32 @@ static void
 log_noexistnodata(void *val, int level, const char *fmt, ...)
        ISC_FORMAT_PRINTF(3, 4);
 
-/*%
- * The structure and functions defined below implement the query logic
- * that previously lived in the single very complex function query_find().
- * The query_ctx_t structure maintains state from function to function.
- * The call flow for the general query processing algorithm is described
- * below:
+#ifdef NS_HOOKS_ENABLE
+
+LIBNS_EXTERNAL_DATA const ns_hook_t *ns__hook_table = NULL;
+
+#define PROCESS_HOOK(...) \
+       NS_PROCESS_HOOK(ns__hook_table, __VA_ARGS__)
+#define PROCESS_HOOK_VOID(...) \
+       NS_PROCESS_HOOK_VOID(ns__hook_table, __VA_ARGS__)
+
+#else
+
+#define PROCESS_HOOK(...)      do {} while (0)
+#define PROCESS_HOOK_VOID(...) do {} while (0)
+
+#endif
+
+/*
+ * The functions defined below implement the query logic that previously lived
+ * in the single very complex function query_find().  The query_ctx_t structure
+ * defined in <ns/query.h> maintains state from function to function.  The call
+ * flow for the general query processing algorithm is described below:
  *
  * 1. Set up query context and other resources for a client
  *    query (query_setup())
  *
- * 2. Start the search (query_start())
+ * 2. Start the search (ns__query_start())
  *
  * 3. Identify authoritative data sources which may have an answer;
  *    search them (query_lookup()). If an answer is found, go to 7.
@@ -310,53 +327,6 @@ log_noexistnodata(void *val, int level, const char *fmt, ...)
  * DNS64, filter-aaaa, RPZ, RRL, and the SERVFAIL cache.)
  */
 
-typedef struct query_ctx {
-       isc_buffer_t *dbuf;                     /* name buffer */
-       dns_name_t *fname;                      /* found name from DB lookup */
-       dns_rdataset_t *rdataset;               /* found rdataset */
-       dns_rdataset_t *sigrdataset;            /* found sigrdataset */
-       dns_rdataset_t *noqname;                /* rdataset needing
-                                                * NOQNAME proof */
-       dns_rdatatype_t qtype;
-       dns_rdatatype_t type;
-
-       unsigned int options;                   /* DB lookup options */
-
-       isc_boolean_t redirected;               /* nxdomain redirected? */
-       isc_boolean_t is_zone;                  /* is DB a zone DB? */
-       isc_boolean_t is_staticstub_zone;
-       isc_boolean_t resuming;                 /* resumed from recursion? */
-       isc_boolean_t dns64, dns64_exclude, rpz;
-       isc_boolean_t authoritative;            /* authoritative query? */
-       isc_boolean_t want_restart;             /* CNAME chain or other
-                                                * restart needed */
-       isc_boolean_t need_wildcardproof;       /* wilcard proof needed */
-       isc_boolean_t nxrewrite;                /* negative answer from RPZ */
-       isc_boolean_t findcoveringnsec;         /* lookup covering NSEC */
-       isc_boolean_t want_stale;               /* want stale records? */
-       dns_fixedname_t wildcardname;           /* name needing wcard proof */
-       dns_fixedname_t dsname;                 /* name needing DS */
-
-       ns_client_t *client;                    /* client object */
-       dns_fetchevent_t *event;                /* recursion event */
-
-       dns_db_t *db;                           /* zone or cache database */
-       dns_dbversion_t *version;               /* DB version */
-       dns_dbnode_t *node;                     /* DB node */
-
-       dns_db_t *zdb;                          /* zone DB values, saved */
-       dns_name_t *zfname;                     /* while searching cache */
-       dns_dbversion_t *zversion;              /* for a better answer */
-       dns_rdataset_t *zrdataset;
-       dns_rdataset_t *zsigrdataset;
-
-       dns_rpz_st_t *rpz_st;                   /* RPZ state */
-       dns_zone_t *zone;                       /* zone to search */
-
-       isc_result_t result;                    /* query result */
-       int line;                               /* line to report error */
-} query_ctx_t;
-
 static void
 query_trace(query_ctx_t *qctx);
 
@@ -367,9 +337,6 @@ qctx_init(ns_client_t *client, dns_fetchevent_t *event,
 static isc_result_t
 query_setup(ns_client_t *client, dns_rdatatype_t qtype);
 
-static isc_result_t
-query_start(query_ctx_t *qctx);
-
 static isc_result_t
 query_lookup(query_ctx_t *qctx);
 
@@ -384,9 +351,6 @@ query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname,
 static isc_result_t
 query_resume(query_ctx_t *qctx);
 
-static isc_result_t
-query_sfcache(query_ctx_t *qctx);
-
 static isc_result_t
 query_checkrrl(query_ctx_t *qctx, isc_result_t result);
 
@@ -5121,12 +5085,12 @@ query_trace(query_ctx_t *qctx) {
 /*
  * Set up query processing for the current query of 'client'.
  * Calls qctx_init() to initialize a query context, checks
- * the SERVFAIL cache, then hands off processing to query_start().
+ * the SERVFAIL cache, then hands off processing to ns__query_start().
  *
  * This is called only from ns_query_start(), to begin a query
  * for the first time.  Restarting an existing query (for
  * instance, to handle CNAME lookups), is done by calling
- * query_start() again with the same query context. Resuming from
+ * ns__query_start() again with the same query context. Resuming from
  * recursion is handled by query_resume().
  */
 static isc_result_t
@@ -5137,14 +5101,6 @@ query_setup(ns_client_t *client, dns_rdatatype_t qtype) {
        qctx_init(client, NULL, qtype, &qctx);
        query_trace(&qctx);
 
-       /*
-        * Check SERVFAIL cache
-        */
-       result = query_sfcache(&qctx);
-       if (result != ISC_R_COMPLETE) {
-               return (result);
-       }
-
        /*
         * If it's a SIG query, we'll iterate the node.
         */
@@ -5152,11 +5108,19 @@ query_setup(ns_client_t *client, dns_rdatatype_t qtype) {
            qctx.qtype == dns_rdatatype_sig)
        {
                qctx.type = dns_rdatatype_any;
-       } else {
-               qctx.type = qctx.qtype;
        }
 
-       return (query_start(&qctx));
+       PROCESS_HOOK(NS_QUERY_SETUP_QCTX_INITIALIZED, &qctx);
+
+       /*
+        * Check SERVFAIL cache
+        */
+       result = ns__query_sfcache(&qctx);
+       if (result != ISC_R_COMPLETE) {
+               return (result);
+       }
+
+       return (ns__query_start(&qctx));
 }
 
 /*%
@@ -5166,10 +5130,10 @@ query_setup(ns_client_t *client, dns_rdatatype_t qtype) {
  * follow a CNAME chain.  Determines which authoritative database to
  * search, then hands off processing to query_lookup().
  */
-static isc_result_t
-query_start(query_ctx_t *qctx) {
+isc_result_t
+ns__query_start(query_ctx_t *qctx) {
        isc_result_t result;
-       CCTRACE(ISC_LOG_DEBUG(3), "query_start");
+       CCTRACE(ISC_LOG_DEBUG(3), "ns__query_start");
        qctx->want_restart = ISC_FALSE;
        qctx->authoritative = ISC_FALSE;
        qctx->version = NULL;
@@ -5206,6 +5170,12 @@ query_start(query_ctx_t *qctx) {
        if (dns_rdatatype_atparent(qctx->qtype) &&
            !dns_name_equal(qctx->client->query.qname, dns_rootname))
        {
+               /*
+                * If authoritative data for this QTYPE is supposed to live in
+                * the parent zone, do not look for an exact match for QNAME,
+                * but rather for its containing zone (unless the QNAME is
+                * root).
+                */
                qctx->options |= DNS_GETDB_NOEXACT;
        }
 
@@ -5218,8 +5188,10 @@ query_start(query_ctx_t *qctx) {
                         (qctx->options & DNS_GETDB_NOEXACT) != 0))
        {
                /*
-                * If the query type is DS, look to see if we are
-                * authoritative for the child zone.
+                * This is a non-recursive QTYPE=DS query with QNAME whose
+                * parent we are not authoritative for.  Check whether we are
+                * authoritative for QNAME, because if so, we need to send a
+                * "no data" response as required by RFC 4035, section 3.1.4.1.
                 */
                dns_db_t *tdb = NULL;
                dns_zone_t *tzone = NULL;
@@ -5232,6 +5204,10 @@ query_start(query_ctx_t *qctx) {
                                          DNS_GETDB_PARTIAL,
                                          &tzone, &tdb, &tversion);
                if (tresult == ISC_R_SUCCESS) {
+                       /*
+                        * We are authoritative for QNAME.  Attach the relevant
+                        * zone to query context, set result to ISC_R_SUCCESS.
+                        */
                        qctx->options &= ~DNS_GETDB_NOEXACT;
                        query_putrdataset(qctx->client, &qctx->rdataset);
                        if (qctx->db != NULL) {
@@ -5247,6 +5223,10 @@ query_start(query_ctx_t *qctx) {
                        qctx->is_zone = ISC_TRUE;
                        result = ISC_R_SUCCESS;
                } else {
+                       /*
+                        * We are not authoritative for QNAME.  Clean up and
+                        * leave result as it was.
+                        */
                        if (tdb != NULL) {
                                dns_db_detach(&tdb);
                        }
@@ -5255,6 +5235,11 @@ query_start(query_ctx_t *qctx) {
                        }
                }
        }
+       /*
+        * If we did not find a database from which we can answer the query,
+        * respond with either REFUSED or SERVFAIL, depending on what the
+        * result of query_getdb() was.
+        */
        if (result != ISC_R_SUCCESS) {
                if (result == DNS_R_REFUSED) {
                        if (WANTRECURSION(qctx->client)) {
@@ -5269,12 +5254,17 @@ query_start(query_ctx_t *qctx) {
                        }
                } else {
                        CCTRACE(ISC_LOG_ERROR,
-                              "query_start: query_getdb failed");
+                              "ns__query_start: query_getdb failed");
                        QUERY_ERROR(qctx, DNS_R_SERVFAIL);
                }
                return (query_done(qctx));
        }
 
+       /*
+        * We found a database from which we can answer the query.  Update
+        * relevant query context flags if the answer is to be prepared using
+        * authoritative data.
+        */
        qctx->is_staticstub_zone = ISC_FALSE;
        if (qctx->is_zone) {
                qctx->authoritative = ISC_TRUE;
@@ -5285,6 +5275,10 @@ query_start(query_ctx_t *qctx) {
                }
        }
 
+       /*
+        * Attach to the database which will be used to prepare the answer.
+        * Update query statistics.
+        */
        if (qctx->event == NULL && qctx->client->query.restarts == 0) {
                if (qctx->is_zone) {
                        if (qctx->zone != NULL) {
@@ -5326,6 +5320,8 @@ query_lookup(query_ctx_t *qctx) {
 
        CCTRACE(ISC_LOG_DEBUG(3), "query_lookup");
 
+       PROCESS_HOOK(NS_QUERY_LOOKUP_BEGIN, qctx);
+
        dns_clientinfomethods_init(&cm, ns_client_sourceip);
        dns_clientinfo_init(&ci, qctx->client, NULL);
 
@@ -5884,10 +5880,11 @@ query_resume(query_ctx_t *qctx) {
  * If the query is recursive, check the SERVFAIL cache to see whether
  * identical queries have failed recently.  If we find a match, and it was
  * from a query with CD=1, *or* if the current query has CD=0, then we just
- * return SERVFAIL again.
+ * return SERVFAIL again.  This prevents a validation failure from eliciting a
+ * SERVFAIL response to a CD=1 query.
  */
-static isc_result_t
-query_sfcache(query_ctx_t *qctx) {
+isc_result_t
+ns__query_sfcache(query_ctx_t *qctx) {
        isc_boolean_t failcache;
        isc_uint32_t flags;
 
@@ -10317,7 +10314,7 @@ query_glueanswer(query_ctx_t *qctx) {
  *
  * - Clean up
  * - If we have an answer ready (positive or negative), send it.
- * - If we need to restart for a chaining query, call query_start() again.
+ * - If we need to restart for a chaining query, call ns__query_start() again.
  * - If we've started recursion, then just clean up; things will be
  *   restarted via fetch_callback()/query_resume().
  */
@@ -10326,6 +10323,8 @@ query_done(query_ctx_t *qctx) {
        const dns_namelist_t *secs = qctx->client->message->sections;
        CCTRACE(ISC_LOG_DEBUG(3), "query_done");
 
+       PROCESS_HOOK(NS_QUERY_DONE_BEGIN, qctx);
+
        /*
         * General cleanup.
         */
@@ -10352,7 +10351,7 @@ query_done(query_ctx_t *qctx) {
         */
        if (qctx->want_restart && qctx->client->query.restarts < MAX_RESTARTS) {
                qctx->client->query.restarts++;
-               return (query_start(qctx));
+               return (ns__query_start(qctx));
        }
 
        if (qctx->want_stale) {
index 44cff74167c570fe2ac4e938a44b6d65766e7ac0..8d3de55d2fadc863ce41f121b36399a27241d896 100644 (file)
@@ -32,11 +32,13 @@ LIBS =              @LIBS@ @ATFLIBS@
 OBJS =         nstest.@O@
 SRCS =         nstest.c \
                listenlist_test.c \
-               notify_test.c
+               notify_test.c \
+               query_test.c
 
 SUBDIRS =
 TARGETS =      listenlist_test@EXEEXT@ \
-               notify_test@EXEEXT@
+               notify_test@EXEEXT@ \
+               query_test
 
 @BIND9_MAKE_RULES@
 
@@ -50,6 +52,11 @@ notify_test@EXEEXT@: notify_test.@O@ nstest.@O@ ${NSDEPLIBS} ${ISCDEPLIBS} ${DNS
                        notify_test.@O@ nstest.@O@ ${NSLIBS} ${DNSLIBS} \
                                ${ISCLIBS} ${LIBS}
 
+query_test@EXEEXT@: query_test.@O@ nstest.@O@ ${NSDEPLIBS} ${ISCDEPLIBS} ${DNSDEPLIBS}
+       ${LIBTOOL_MODE_LINK} ${PURIFY} ${CC} ${CFLAGS} ${LDFLAGS} -o $@ \
+                       query_test.@O@ nstest.@O@ ${NSLIBS} ${DNSLIBS} \
+                               ${ISCLIBS} ${LIBS}
+
 unit::
        sh ${top_srcdir}/unit/unittest.sh
 
index 92b9d6f311199187d00c349d5721fa952b06812f..2e68781b001c290ae433f7d90d18f9077625ef03 100644 (file)
 
 #include "nstest.h"
 
-static dns_zone_t *zone = NULL;
-static dns_view_t *view = NULL;
-
-/*
- * Helper functions
- */
-static void
-setup_zone(const char *zonename, const char *filename) {
-       isc_result_t result;
-       dns_db_t *db = NULL;
-
-       result = ns_test_makezone(zonename, &zone, NULL, ISC_TRUE);
-       ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
-       result = ns_test_setupzonemgr();
-       ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
-       result = ns_test_managezone(zone);
-       ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
-       view = dns_zone_getview(zone);
-       ATF_REQUIRE(view->zonetable != NULL);
-       view->nocookieudp = 512;
-
-       dns_zone_setfile(zone, filename);
-       result = dns_zone_load(zone);
-       ATF_REQUIRE(result == ISC_R_SUCCESS);
-
-       /* The zone should now be loaded; test it */
-       result = dns_zone_getdb(zone, &db);
-       ATF_CHECK_EQ(result, ISC_R_SUCCESS);
-       ATF_CHECK(db != NULL);
-       if (db != NULL) {
-               dns_db_detach(&db);
-       }
-}
-
-static void
-cleanup_zone() {
-       ns_test_releasezone(zone);
-       ns_test_closezonemgr();
-
-       dns_zone_detach(&zone);
-       dns_view_detach(&view);
-}
-
 static void
 check_response(isc_buffer_t *buf) {
        isc_result_t result;
@@ -113,7 +70,12 @@ ATF_TC_BODY(notify_start, tc) {
        result = ns_test_getclient(NULL, ISC_FALSE, &client);
        ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
 
-       setup_zone("example.com", "testdata/notify/zone1.db");
+       result = ns_test_makeview("view", ISC_FALSE, &client->view);
+       ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
+
+       result = ns_test_serve_zone("example.com", "testdata/notify/zone1.db",
+                                   client->view);
+       ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
 
        /*
         * Create a NOTIFY message by parsing a file in testdata.
@@ -136,7 +98,6 @@ ATF_TC_BODY(notify_start, tc) {
         * Set up client object with this message and test the NOTIFY
         * handler.
         */
-       dns_view_attach(view, &client->view);
        if (client->message != NULL) {
                dns_message_destroy(&client->message);
        }
@@ -148,7 +109,7 @@ ATF_TC_BODY(notify_start, tc) {
        /*
         * Clean up
         */
-       cleanup_zone();
+       ns_test_cleanup_zone();
 
        ns_client_detach(&client);
 
index 4a9f20e46441198513b51a7c6f8f97c067614d19..a47ee8aea0b656bc4495edefe5b96943b96f83c7 100644 (file)
@@ -22,6 +22,7 @@
 #include <isc/mem.h>
 #include <isc/os.h>
 #include <isc/print.h>
+#include <isc/random.h>
 #include <isc/string.h>
 #include <isc/socket.h>
 #include <isc/stdio.h>
@@ -29,6 +30,7 @@
 #include <isc/timer.h>
 #include <isc/util.h>
 
+#include <dns/cache.h>
 #include <dns/db.h>
 #include <dns/dispatch.h>
 #include <dns/fixedname.h>
@@ -42,6 +44,8 @@
 #include <ns/interfacemgr.h>
 #include <ns/server.h>
 
+#include "../hooks.h"
+
 #include "nstest.h"
 
 isc_mem_t *mctx = NULL;
@@ -63,6 +67,8 @@ isc_boolean_t run_managers = ISC_FALSE;
 
 static isc_boolean_t hash_active = ISC_FALSE, dst_active = ISC_FALSE;
 
+static dns_zone_t *served_zone = NULL;
+
 /*
  * Logging categories: this needs to match the list in lib/ns/log.c.
  */
@@ -275,6 +281,8 @@ ns_test_begin(FILE *logfile, isc_boolean_t start_managers) {
        if (chdir(TESTS) == -1)
                CHECK(ISC_R_FAILURE);
 
+       ns__hook_table = NULL;
+
        return (ISC_R_SUCCESS);
 
   cleanup:
@@ -306,15 +314,29 @@ ns_test_end(void) {
                isc_mem_destroy(&mctx);
 }
 
-/*
- * Create a view.
- */
 isc_result_t
-ns_test_makeview(const char *name, dns_view_t **viewp) {
-       isc_result_t result;
+ns_test_makeview(const char *name, isc_boolean_t with_cache,
+                dns_view_t **viewp)
+{
+       dns_cache_t *cache = NULL;
        dns_view_t *view = NULL;
+       isc_result_t result;
 
        CHECK(dns_view_create(mctx, dns_rdataclass_in, name, &view));
+
+       if (with_cache) {
+               CHECK(dns_cache_create(mctx, taskmgr, timermgr,
+                                      dns_rdataclass_in, "rbt", 0, NULL,
+                                      &cache));
+               dns_view_setcache(view, cache);
+               /*
+                * Reference count for "cache" is now at 2, so decrement it in
+                * order for the cache to be automatically freed when "view"
+                * gets freed.
+                */
+               dns_cache_detach(&cache);
+       }
+
        *viewp = view;
 
        return (ISC_R_SUCCESS);
@@ -417,6 +439,79 @@ ns_test_closezonemgr(void) {
        dns_zonemgr_detach(&zonemgr);
 }
 
+isc_result_t
+ns_test_serve_zone(const char *zonename, const char *filename,
+                  dns_view_t *view)
+{
+       isc_result_t result;
+       dns_db_t *db = NULL;
+
+       /*
+        * Prepare zone structure for further processing.
+        */
+       result = ns_test_makezone(zonename, &served_zone, view, ISC_TRUE);
+       if (result != ISC_R_SUCCESS) {
+               return (result);
+       }
+
+       /*
+        * Start zone manager.
+        */
+       result = ns_test_setupzonemgr();
+       if (result != ISC_R_SUCCESS) {
+               goto free_zone;
+       }
+
+       /*
+        * Add the zone to the zone manager.
+        */
+       result = ns_test_managezone(served_zone);
+       if (result != ISC_R_SUCCESS) {
+               goto close_zonemgr;
+       }
+
+       view->nocookieudp = 512;
+
+       /*
+        * Set path to the master file for the zone and then load it.
+        */
+       dns_zone_setfile(served_zone, filename);
+       result = dns_zone_load(served_zone);
+       if (result != ISC_R_SUCCESS) {
+               goto release_zone;
+       }
+
+       /*
+        * The zone should now be loaded; test it.
+        */
+       result = dns_zone_getdb(served_zone, &db);
+       if (result != ISC_R_SUCCESS) {
+               goto release_zone;
+       }
+       if (db != NULL) {
+               dns_db_detach(&db);
+       }
+
+       return (ISC_R_SUCCESS);
+
+release_zone:
+       ns_test_releasezone(served_zone);
+close_zonemgr:
+       ns_test_closezonemgr();
+free_zone:
+       dns_zone_detach(&served_zone);
+
+       return (result);
+}
+
+void
+ns_test_cleanup_zone(void) {
+       ns_test_releasezone(served_zone);
+       ns_test_closezonemgr();
+
+       dns_zone_detach(&served_zone);
+}
+
 isc_result_t
 ns_test_getclient(ns_interface_t *ifp0, isc_boolean_t tcp,
                  ns_client_t **clientp)
@@ -435,6 +530,291 @@ ns_test_getclient(ns_interface_t *ifp0, isc_boolean_t tcp,
        return (result);
 }
 
+/*%
+ * Synthesize a DNS message based on supplied QNAME, QTYPE and flags, then
+ * parse it and store the results in client->message.
+ */
+static isc_result_t
+attach_query_msg_to_client(ns_client_t *client, const char *qnamestr,
+                          dns_rdatatype_t qtype, unsigned int qflags)
+{
+       dns_rdataset_t *qrdataset = NULL;
+       dns_message_t *message = NULL;
+       unsigned char query[65536];
+       dns_name_t *qname = NULL;
+       isc_buffer_t querybuf;
+       dns_compress_t cctx;
+       isc_result_t result;
+       isc_uint32_t qid;
+
+       REQUIRE(client != NULL);
+       REQUIRE(qnamestr != NULL);
+
+       /*
+        * Create a new DNS message holding a query.
+        */
+       result = dns_message_create(mctx, DNS_MESSAGE_INTENTRENDER, &message);
+       if (result != ISC_R_SUCCESS) {
+               return (result);
+       }
+
+       /*
+        * Set query ID to a random value.
+        */
+       isc_random_get(&qid);
+       message->id = (dns_messageid_t)(qid & 0xffff);
+
+       /*
+        * Set query flags as requested by the caller.
+        */
+       message->flags = qflags;
+
+       /*
+        * Allocate structures required to construct the query.
+        */
+       result = dns_message_gettemprdataset(message, &qrdataset);
+       if (result != ISC_R_SUCCESS) {
+               goto destroy_message;
+       }
+       result = dns_message_gettempname(message, &qname);
+       if (result != ISC_R_SUCCESS) {
+               goto put_rdataset;
+       }
+
+       /*
+        * Convert "qnamestr" to a DNS name, create a question rdataset of
+        * class IN and type "qtype", link the two and add the result to the
+        * QUESTION section of the query.
+        */
+       result = dns_name_fromstring(qname, qnamestr, 0, mctx);
+       if (result != ISC_R_SUCCESS) {
+               goto put_name;
+       }
+       dns_rdataset_makequestion(qrdataset, dns_rdataclass_in, qtype);
+       ISC_LIST_APPEND(qname->list, qrdataset, link);
+       dns_message_addname(message, qname, DNS_SECTION_QUESTION);
+
+       /*
+        * Render the query.
+        */
+       dns_compress_init(&cctx, -1, mctx);
+       isc_buffer_init(&querybuf, query, sizeof(query));
+       result = dns_message_renderbegin(message, &cctx, &querybuf);
+       if (result != ISC_R_SUCCESS) {
+               goto destroy_message;
+       }
+       result = dns_message_rendersection(message, DNS_SECTION_QUESTION, 0);
+       if (result != ISC_R_SUCCESS) {
+               goto destroy_message;
+       }
+       result = dns_message_renderend(message);
+       if (result != ISC_R_SUCCESS) {
+               goto destroy_message;
+       }
+       dns_compress_invalidate(&cctx);
+
+       /*
+        * Destroy the created message as it was rendered into "querybuf" and
+        * the latter is all we are going to need from now on.
+        */
+       dns_message_destroy(&message);
+
+       /*
+        * Parse the rendered query, storing results in client->message.
+        */
+       isc_buffer_first(&querybuf);
+       return (dns_message_parse(client->message, &querybuf, 0));
+
+put_name:
+       dns_message_puttempname(message, &qname);
+put_rdataset:
+       dns_message_puttemprdataset(message, &qrdataset);
+destroy_message:
+       dns_message_destroy(&message);
+
+       return (result);
+}
+
+/*%
+ * A hook callback which stores the query context pointed to by "hook_data" at
+ * "callback_data".  Causes execution to be interrupted at hook insertion
+ * point.
+ */
+static isc_boolean_t
+extract_qctx(void *hook_data, void *callback_data, isc_result_t *resultp) {
+       query_ctx_t **qctxp;
+       query_ctx_t *qctx;
+
+       REQUIRE(hook_data != NULL);
+       REQUIRE(callback_data != NULL);
+       REQUIRE(resultp != NULL);
+
+       /*
+        * qctx is a stack variable in lib/ns/query.c.  Its contents need to be
+        * duplicated or otherwise they will become invalidated once the stack
+        * gets unwound.
+        */
+       qctx = isc_mem_get(mctx, sizeof(*qctx));
+       if (qctx != NULL) {
+               memmove(qctx, (query_ctx_t *)hook_data, sizeof(*qctx));
+       }
+
+       qctxp = (query_ctx_t **)callback_data;
+       /*
+        * If memory allocation failed, the supplied pointer will simply be set
+        * to NULL.  We rely on the user of this hook to react properly.
+        */
+       *qctxp = qctx;
+       *resultp = ISC_R_UNSET;
+
+       return (ISC_TRUE);
+}
+
+/*%
+ * Initialize a query context for "client" and store it in "qctxp".
+ *
+ * Requires:
+ *
+ *     \li "client->message" to hold a parsed DNS query.
+ */
+static isc_result_t
+create_qctx_for_client(ns_client_t *client, query_ctx_t **qctxp) {
+       const ns_hook_t *saved_hook_table;
+       const ns_hook_t query_hooks[NS_QUERY_HOOKS_COUNT] = {
+               [NS_QUERY_SETUP_QCTX_INITIALIZED] = {
+                       .callback = extract_qctx,
+                       .callback_data = qctxp,
+               },
+       };
+
+       REQUIRE(client != NULL);
+       REQUIRE(qctxp != NULL);
+       REQUIRE(*qctxp == NULL);
+
+       /*
+        * Call ns_query_start() to initialize a query context for given
+        * client, but first hook into query_setup() so that we can just
+        * extract an initialized query context, without kicking off any
+        * further processing.  Make sure we do not overwrite any previously
+        * set hooks.
+        */
+       saved_hook_table = ns__hook_table;
+       ns__hook_table = query_hooks;
+       ns_query_start(client);
+       ns__hook_table = saved_hook_table;
+
+       if (*qctxp == NULL) {
+               return (ISC_R_NOMEMORY);
+       } else {
+               return (ISC_R_SUCCESS);
+       }
+}
+
+isc_result_t
+ns_test_qctx_create(const ns_test_qctx_create_params_t *params,
+                   query_ctx_t **qctxp)
+{
+       ns_client_t *client = NULL;
+       isc_result_t result;
+
+       REQUIRE(params != NULL);
+       REQUIRE(params->qname != NULL);
+       REQUIRE(qctxp != NULL);
+       REQUIRE(*qctxp == NULL);
+
+       /*
+        * Allocate and initialize a client structure.
+        */
+       result = ns_test_getclient(NULL, ISC_FALSE, &client);
+       if (result != ISC_R_SUCCESS) {
+               return (result);
+       }
+       TIME_NOW(&client->tnow);
+
+       /*
+        * Every client needs to belong to a view.
+        */
+       result = ns_test_makeview("view", params->with_cache, &client->view);
+       if (result != ISC_R_SUCCESS) {
+               goto detach_client;
+       }
+
+       /*
+        * Synthesize a DNS query using given QNAME, QTYPE and flags, storing
+        * it in client->message.
+        */
+       result = attach_query_msg_to_client(client, params->qname,
+                                           params->qtype, params->qflags);
+       if (result != ISC_R_SUCCESS) {
+               goto detach_client;
+       }
+
+       /*
+        * Allow recursion for the client.  As NS_CLIENTATTR_RA normally gets
+        * set in ns__client_request(), i.e. earlier than the unit tests hook
+        * into the call chain, just set it manually.
+        */
+       client->attributes |= NS_CLIENTATTR_RA;
+
+       /*
+        * Create a query context for a client sending the previously
+        * synthesized query.
+        */
+       result = create_qctx_for_client(client, qctxp);
+       if (result != ISC_R_SUCCESS) {
+               goto destroy_query;
+       }
+
+       /*
+        * Reference count for "client" is now at 2, so decrement it in order
+        * for it to drop to zero when "qctx" gets destroyed.
+        */
+       ns_client_detach(&client);
+
+       return (ISC_R_SUCCESS);
+
+destroy_query:
+       dns_message_destroy(&client->message);
+detach_client:
+       ns_client_detach(&client);
+
+       return (result);
+}
+
+void
+ns_test_qctx_destroy(query_ctx_t **qctxp) {
+       query_ctx_t *qctx;
+
+       REQUIRE(qctxp != NULL);
+       REQUIRE(*qctxp != NULL);
+
+       qctx = *qctxp;
+
+       ns_client_detach(&qctx->client);
+
+       if (qctx->zone != NULL) {
+               dns_zone_detach(&qctx->zone);
+       }
+       if (qctx->db != NULL) {
+               dns_db_detach(&qctx->db);
+       }
+
+       isc_mem_put(mctx, qctx, sizeof(*qctx));
+       *qctxp = NULL;
+}
+
+isc_boolean_t
+ns_test_hook_catch_call(void *hook_data, void *callback_data,
+                       isc_result_t *resultp)
+{
+       UNUSED(hook_data);
+       UNUSED(callback_data);
+
+       *resultp = ISC_R_UNSET;
+
+       return (ISC_TRUE);
+}
+
 /*
  * Sleep for 'usec' microseconds.
  */
index 9b7f37cb2a67ce1f301ad0f6084b5e8c3ca7aa8b..5d74deaabb4c5b15b293305e2f18801927a34bc4 100644 (file)
 #include <ns/interfacemgr.h>
 #include <ns/client.h>
 
+typedef struct ns_test_id {
+       const char *description;
+       int lineno;
+} ns_test_id_t;
+
+#define NS_TEST_ID(desc)       { .description = desc, .lineno = __LINE__ }
+
 #define CHECK(r) \
        do { \
                result = (r); \
@@ -55,8 +62,13 @@ ns_test_begin(FILE *logfile, isc_boolean_t create_managers);
 void
 ns_test_end(void);
 
+/*%
+ * Create a view.  If "with_cache" is set to ISC_TRUE, a cache database will
+ * also be created and attached to the created view.
+ */
 isc_result_t
-ns_test_makeview(const char *name, dns_view_t **viewp);
+ns_test_makeview(const char *name, isc_boolean_t with_cache,
+                dns_view_t **viewp);
 
 isc_result_t
 ns_test_makezone(const char *name, dns_zone_t **zonep, dns_view_t *view,
@@ -74,6 +86,21 @@ ns_test_releasezone(dns_zone_t *zone);
 void
 ns_test_closezonemgr(void);
 
+/*%
+ * Load data for zone "zonename" from file "filename" and start serving it to
+ * clients matching "view".  Only one zone loaded using this function can be
+ * served at any given time.
+ */
+isc_result_t
+ns_test_serve_zone(const char *zonename, const char *filename,
+                  dns_view_t *view);
+
+/*%
+ * Release the zone loaded by ns_test_serve_zone().
+ */
+void
+ns_test_cleanup_zone(void);
+
 void
 ns_test_nap(isc_uint32_t usec);
 
@@ -88,3 +115,37 @@ ns_test_getdata(const char *file, unsigned char *buf,
 isc_result_t
 ns_test_getclient(ns_interface_t *ifp0, isc_boolean_t tcp,
                  ns_client_t **clientp);
+
+/*%
+ * Structure containing parameters for ns_test_qctx_create().
+ */
+typedef struct ns_test_qctx_create_params {
+       const char *qname;
+       dns_rdatatype_t qtype;
+       unsigned int qflags;
+       isc_boolean_t with_cache;
+} ns_test_qctx_create_params_t;
+
+/*%
+ * Prepare a query context identical with one that would be prepared if a query
+ * with given QNAME, QTYPE and flags was received from a client.  Recursion is
+ * assumed to be allowed for this client.  If "with_cache" is set to ISC_TRUE,
+ * a cache database will be created and associated with the view matching the
+ * incoming query.
+ */
+isc_result_t
+ns_test_qctx_create(const ns_test_qctx_create_params_t *params,
+                   query_ctx_t **qctxp);
+
+/*%
+ * Destroy a query context created by ns_test_qctx_create().
+ */
+void
+ns_test_qctx_destroy(query_ctx_t **qctxp);
+
+/*%
+ * A hook callback interrupting execution at given hook's insertion point.
+ */
+isc_boolean_t
+ns_test_hook_catch_call(void *hook_data, void *callback_data,
+                       isc_result_t *resultp);
diff --git a/lib/ns/tests/query_test.c b/lib/ns/tests/query_test.c
new file mode 100644 (file)
index 0000000..7906989
--- /dev/null
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2017  Internet Systems Consortium, Inc. ("ISC")
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/*! \file */
+
+#include <config.h>
+
+#include <atf-c.h>
+
+#include <dns/badcache.h>
+#include <dns/view.h>
+#include <isc/util.h>
+#include <ns/client.h>
+#include <ns/query.h>
+
+#include "../hooks.h"
+
+#include "nstest.h"
+
+/*****
+ ***** ns__query_sfcache() tests
+ *****/
+
+/*%
+ * Structure containing parameters for ns__query_sfcache_test().
+ */
+typedef struct {
+       const ns_test_id_t id;             /* libns test identifier */
+       unsigned int qflags;               /* query flags */
+       isc_boolean_t cache_entry_present; /* whether a SERVFAIL cache entry
+                                             matching the query should be
+                                             present */
+       isc_uint32_t cache_entry_flags;    /* NS_FAILCACHE_* flags to set for
+                                             the SERVFAIL cache entry */
+       isc_boolean_t servfail_expected;   /* whether a cached SERVFAIL is
+                                             expected to be returned */
+} ns__query_sfcache_test_params_t;
+
+/*%
+ * Perform a single ns__query_sfcache() check using given parameters.
+ */
+static void
+ns__query_sfcache_test(const ns__query_sfcache_test_params_t *test) {
+       query_ctx_t *qctx = NULL;
+       isc_result_t result;
+
+       REQUIRE(test != NULL);
+       REQUIRE(test->id.description != NULL);
+       REQUIRE(test->cache_entry_present == ISC_TRUE ||
+               test->cache_entry_flags == 0);
+
+       /*
+        * Interrupt execution if query_done() is called.
+        */
+       const ns_hook_t query_hooks[NS_QUERY_HOOKS_COUNT] = {
+               [NS_QUERY_DONE_BEGIN] = {
+                       .callback = ns_test_hook_catch_call,
+                       .callback_data = NULL,
+               },
+       };
+       ns__hook_table = query_hooks;
+
+       /*
+        * Construct a query context for a ./NS query with given flags.
+        */
+       {
+               const ns_test_qctx_create_params_t qctx_params = {
+                       .qname = ".",
+                       .qtype = dns_rdatatype_ns,
+                       .qflags = test->qflags,
+                       .with_cache = ISC_TRUE,
+               };
+
+               result = ns_test_qctx_create(&qctx_params, &qctx);
+               ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
+       }
+
+       /*
+        * If this test wants a SERVFAIL cache entry matching the query to
+        * exist, create it.
+        */
+       if (test->cache_entry_present) {
+               isc_interval_t hour;
+               isc_time_t expire;
+
+               isc_interval_set(&hour, 3600, 0);
+               result = isc_time_nowplusinterval(&expire, &hour);
+               ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
+
+               dns_badcache_add(qctx->client->view->failcache, dns_rootname,
+                                dns_rdatatype_ns, ISC_TRUE,
+                                test->cache_entry_flags, &expire);
+       }
+
+       /*
+        * Check whether ns__query_sfcache() behaves as expected.
+        */
+       ns__query_sfcache(qctx);
+
+       if (test->servfail_expected) {
+               ATF_CHECK_EQ_MSG(qctx->result, DNS_R_SERVFAIL,
+                                "test \"%s\" on line %d: "
+                                "expected SERVFAIL, got %s",
+                                test->id.description, test->id.lineno,
+                                isc_result_totext(qctx->result));
+       } else {
+               ATF_CHECK_EQ_MSG(qctx->result, ISC_R_SUCCESS,
+                                "test \"%s\" on line %d: "
+                                "expected success, got %s",
+                                test->id.description, test->id.lineno,
+                                isc_result_totext(qctx->result));
+       }
+
+       /*
+        * Clean up.
+        */
+       ns_test_qctx_destroy(&qctx);
+
+}
+
+ATF_TC(ns__query_sfcache);
+ATF_TC_HEAD(ns__query_sfcache, tc) {
+       atf_tc_set_md_var(tc, "descr", "ns__query_sfcache()");
+}
+ATF_TC_BODY(ns__query_sfcache, tc) {
+       isc_result_t result;
+       size_t i;
+
+       const ns__query_sfcache_test_params_t tests[] = {
+               /*
+                * Sanity check for an empty SERVFAIL cache.
+                */
+               {
+                       NS_TEST_ID("query: RD=1, CD=0; cache: empty"),
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .cache_entry_present = ISC_FALSE,
+                       .servfail_expected = ISC_FALSE,
+               },
+               /*
+                * Query: RD=1, CD=0.  Cache entry: CD=0.  Should SERVFAIL.
+                */
+               {
+                       NS_TEST_ID("query: RD=1, CD=0; cache: CD=0"),
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .cache_entry_present = ISC_TRUE,
+                       .cache_entry_flags = 0,
+                       .servfail_expected = ISC_TRUE,
+               },
+               /*
+                * Query: RD=1, CD=1.  Cache entry: CD=0.  Should not SERVFAIL:
+                * failed validation should not influence CD=1 queries.
+                */
+               {
+                       NS_TEST_ID("query: RD=1, CD=1; cache: CD=0"),
+                       .qflags = DNS_MESSAGEFLAG_RD | DNS_MESSAGEFLAG_CD,
+                       .cache_entry_present = ISC_TRUE,
+                       .cache_entry_flags = 0,
+                       .servfail_expected = ISC_FALSE,
+               },
+               /*
+                * Query: RD=1, CD=1.  Cache entry: CD=1.  Should SERVFAIL:
+                * SERVFAIL responses elicited by CD=1 queries can be
+                * "replayed" for other CD=1 queries during the lifetime of the
+                * SERVFAIL cache entry.
+                */
+               {
+                       NS_TEST_ID("query: RD=1, CD=1; cache: CD=1"),
+                       .qflags = DNS_MESSAGEFLAG_RD | DNS_MESSAGEFLAG_CD,
+                       .cache_entry_present = ISC_TRUE,
+                       .cache_entry_flags = NS_FAILCACHE_CD,
+                       .servfail_expected = ISC_TRUE,
+               },
+               /*
+                * Query: RD=1, CD=0.  Cache entry: CD=1.  Should SERVFAIL: if
+                * a CD=1 query elicited a SERVFAIL, a CD=0 query for the same
+                * QNAME and QTYPE will SERVFAIL as well.
+                */
+               {
+                       NS_TEST_ID("query: RD=1, CD=0; cache: CD=1"),
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .cache_entry_present = ISC_TRUE,
+                       .cache_entry_flags = NS_FAILCACHE_CD,
+                       .servfail_expected = ISC_TRUE,
+               },
+               /*
+                * Query: RD=0, CD=0.  Cache entry: CD=0.  Should not SERVFAIL
+                * despite a matching entry being present as the SERVFAIL cache
+                * should not be consulted for non-recursive queries.
+                */
+               {
+                       NS_TEST_ID("query: RD=0, CD=0; cache: CD=0"),
+                       .qflags = 0,
+                       .cache_entry_present = ISC_TRUE,
+                       .cache_entry_flags = 0,
+                       .servfail_expected = ISC_FALSE,
+               },
+       };
+
+       UNUSED(tc);
+
+       result = ns_test_begin(NULL, ISC_TRUE);
+       ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
+
+       for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
+               ns__query_sfcache_test(&tests[i]);
+       }
+
+       ns_test_end();
+}
+
+/*****
+ ***** ns__query_start() tests
+ *****/
+
+/*%
+ * Structure containing parameters for ns__query_start_test().
+ */
+typedef struct {
+       const ns_test_id_t id;             /* libns test identifier */
+       const char *qname;                 /* QNAME */
+       dns_rdatatype_t qtype;             /* QTYPE */
+       unsigned int qflags;               /* query flags */
+       isc_boolean_t disable_name_checks; /* if set to ISC_TRUE, owner name
+                                             checks will be disabled for the
+                                             view created */
+       isc_boolean_t recursive_service;   /* if set to ISC_TRUE, the view
+                                             created will have a cache
+                                             database attached */
+       const char *auth_zone_origin;      /* origin name of the zone the
+                                             created view will be
+                                             authoritative for */
+       const char *auth_zone_path;        /* path to load the authoritative
+                                             zone from */
+       enum {                             /* expected result: */
+               NS__QUERY_START_R_INVALID,
+               NS__QUERY_START_R_REFUSE,  /* query should be REFUSED */
+               NS__QUERY_START_R_CACHE,   /* query should be answered from
+                                             cache */
+               NS__QUERY_START_R_AUTH,    /* query should be answered using
+                                             authoritative data */
+       } expected_result;
+} ns__query_start_test_params_t;
+
+/*%
+ * Perform a single ns__query_start() check using given parameters.
+ */
+static void
+ns__query_start_test(const ns__query_start_test_params_t *test) {
+       query_ctx_t *qctx = NULL;
+       isc_result_t result;
+
+       REQUIRE(test != NULL);
+       REQUIRE(test->id.description != NULL);
+       REQUIRE((test->auth_zone_origin == NULL &&
+                test->auth_zone_path == NULL) ||
+               (test->auth_zone_origin != NULL &&
+                test->auth_zone_path != NULL));
+
+       /*
+        * Interrupt execution if query_lookup() or query_done() is called.
+        */
+       const ns_hook_t query_hooks[NS_QUERY_HOOKS_COUNT] = {
+               [NS_QUERY_LOOKUP_BEGIN] = {
+                       .callback = ns_test_hook_catch_call,
+                       .callback_data = NULL,
+               },
+               [NS_QUERY_DONE_BEGIN] = {
+                       .callback = ns_test_hook_catch_call,
+                       .callback_data = NULL,
+               },
+       };
+       ns__hook_table = query_hooks;
+
+       /*
+        * Construct a query context using the supplied parameters.
+        */
+       {
+               const ns_test_qctx_create_params_t qctx_params = {
+                       .qname = test->qname,
+                       .qtype = test->qtype,
+                       .qflags = test->qflags,
+                       .with_cache = test->recursive_service,
+               };
+               result = ns_test_qctx_create(&qctx_params, &qctx);
+               ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
+       }
+
+       /*
+        * Enable view->checknames by default, disable if requested.
+        */
+       qctx->client->view->checknames = !test->disable_name_checks;
+
+       /*
+        * Load zone from file and attach it to the client's view, if
+        * requested.
+        */
+       if (test->auth_zone_path != NULL) {
+               result = ns_test_serve_zone(test->auth_zone_origin,
+                                           test->auth_zone_path,
+                                           qctx->client->view);
+               ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
+       }
+
+       /*
+        * Check whether ns__query_start() behaves as expected.
+        */
+       ns__query_start(qctx);
+
+       switch (test->expected_result) {
+       case NS__QUERY_START_R_REFUSE:
+               ATF_CHECK_EQ_MSG(qctx->result, DNS_R_REFUSED,
+                                "test \"%s\" on line %d: "
+                                "expected REFUSED, got %s",
+                                test->id.description, test->id.lineno,
+                                isc_result_totext(qctx->result));
+               ATF_CHECK_EQ_MSG(qctx->zone, NULL,
+                                "test \"%s\" on line %d: "
+                                "no zone was expected to be attached to "
+                                "query context, but some was",
+                                test->id.description, test->id.lineno);
+               ATF_CHECK_EQ_MSG(qctx->db, NULL,
+                                "test \"%s\" on line %d: "
+                                "no database was expected to be attached to "
+                                "query context, but some was",
+                                test->id.description, test->id.lineno);
+               break;
+       case NS__QUERY_START_R_CACHE:
+               ATF_CHECK_EQ_MSG(qctx->result, ISC_R_SUCCESS,
+                                "test \"%s\" on line %d: "
+                                "expected success, got %s",
+                                test->id.description, test->id.lineno,
+                                isc_result_totext(qctx->result));
+               ATF_CHECK_EQ_MSG(qctx->zone, NULL,
+                                "test \"%s\" on line %d: "
+                                "no zone was expected to be attached to "
+                                "query context, but some was",
+                                test->id.description, test->id.lineno);
+               ATF_CHECK_MSG((qctx->db != NULL &&
+                              qctx->db == qctx->client->view->cachedb),
+                             "test \"%s\" on line %d: "
+                             "cache database was expected to be attached to "
+                             "query context, but it was not",
+                             test->id.description, test->id.lineno);
+               break;
+       case NS__QUERY_START_R_AUTH:
+               ATF_CHECK_EQ_MSG(qctx->result, ISC_R_SUCCESS,
+                                "test \"%s\" on line %d: "
+                                "expected success, got %s",
+                                test->id.description, test->id.lineno,
+                                isc_result_totext(qctx->result));
+               ATF_CHECK_MSG(qctx->zone != NULL,
+                             "test \"%s\" on line %d: "
+                             "a zone was expected to be attached to query "
+                             "context, but it was not",
+                             test->id.description, test->id.lineno);
+               ATF_CHECK_MSG((qctx->db != NULL &&
+                              qctx->db != qctx->client->view->cachedb),
+                             "test \"%s\" on line %d: "
+                             "cache database was not expected to be attached "
+                             "to query context, but it is",
+                             test->id.description, test->id.lineno);
+               break;
+       case NS__QUERY_START_R_INVALID:
+               ATF_REQUIRE_MSG(ISC_FALSE,
+                               "test \"%s\" on line %d has no expected "
+                               "result set",
+                               test->id.description, test->id.lineno);
+               break;
+       default:
+               INSIST(0);
+               break;
+       }
+
+       /*
+        * Clean up.
+        */
+       if (test->auth_zone_path != NULL) {
+               ns_test_cleanup_zone();
+       }
+       ns_test_qctx_destroy(&qctx);
+}
+
+ATF_TC(ns__query_start);
+ATF_TC_HEAD(ns__query_start, tc) {
+       atf_tc_set_md_var(tc, "descr", "ns__query_start()");
+}
+ATF_TC_BODY(ns__query_start, tc) {
+       isc_result_t result;
+       size_t i;
+
+       const ns__query_start_test_params_t tests[] = {
+               /*
+                * Recursive foo/A query to a server without recursive service
+                * and no zones configured.  Query should be REFUSED.
+                */
+               {
+                       NS_TEST_ID("foo/A, no cache, no auth"),
+                       .qname = "foo",
+                       .qtype = dns_rdatatype_a,
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .recursive_service = ISC_FALSE,
+                       .expected_result = NS__QUERY_START_R_REFUSE,
+               },
+               /*
+                * Recursive foo/A query to a server with recursive service and
+                * no zones configured.  Query should be answered from cache.
+                */
+               {
+                       NS_TEST_ID("foo/A, cache, no auth"),
+                       .qname = "foo",
+                       .qtype = dns_rdatatype_a,
+                       .recursive_service = ISC_TRUE,
+                       .expected_result = NS__QUERY_START_R_CACHE,
+               },
+               /*
+                * Recursive foo/A query to a server with recursive service and
+                * zone "foo" configured.  Query should be answered from
+                * authoritative data.
+                */
+               {
+                       NS_TEST_ID("foo/A, RD=1, cache, auth for foo"),
+                       .qname = "foo",
+                       .qtype = dns_rdatatype_a,
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .recursive_service = ISC_TRUE,
+                       .auth_zone_origin = "foo",
+                       .auth_zone_path = "testdata/query/foo.db",
+                       .expected_result = NS__QUERY_START_R_AUTH,
+               },
+               /*
+                * Recursive bar/A query to a server without recursive service
+                * and zone "foo" configured.  Query should be REFUSED.
+                */
+               {
+                       NS_TEST_ID("bar/A, RD=1, no cache, auth for foo"),
+                       .qname = "bar",
+                       .qtype = dns_rdatatype_a,
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .recursive_service = ISC_FALSE,
+                       .auth_zone_origin = "foo",
+                       .auth_zone_path = "testdata/query/foo.db",
+                       .expected_result = NS__QUERY_START_R_REFUSE,
+               },
+               /*
+                * Recursive bar/A query to a server with recursive service and
+                * zone "foo" configured.  Query should be answered from
+                * cache.
+                */
+               {
+                       NS_TEST_ID("bar/A, RD=1, cache, auth for foo"),
+                       .qname = "bar",
+                       .qtype = dns_rdatatype_a,
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .recursive_service = ISC_TRUE,
+                       .auth_zone_origin = "foo",
+                       .auth_zone_path = "testdata/query/foo.db",
+                       .expected_result = NS__QUERY_START_R_CACHE,
+               },
+               /*
+                * Recursive bar.foo/DS query to a server with recursive
+                * service and zone "foo" configured.  Query should be answered
+                * from authoritative data.
+                */
+               {
+                       NS_TEST_ID("bar.foo/DS, RD=1, cache, auth for foo"),
+                       .qname = "bar.foo",
+                       .qtype = dns_rdatatype_ds,
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .recursive_service = ISC_TRUE,
+                       .auth_zone_origin = "foo",
+                       .auth_zone_path = "testdata/query/foo.db",
+                       .expected_result = NS__QUERY_START_R_AUTH,
+               },
+               /*
+                * Non-recursive bar.foo/DS query to a server with recursive
+                * service and zone "foo" configured.  Query should be answered
+                * from authoritative data.
+                */
+               {
+                       NS_TEST_ID("bar.foo/DS, RD=0, cache, auth for foo"),
+                       .qname = "bar.foo",
+                       .qtype = dns_rdatatype_ds,
+                       .qflags = 0,
+                       .recursive_service = ISC_TRUE,
+                       .auth_zone_origin = "foo",
+                       .auth_zone_path = "testdata/query/foo.db",
+                       .expected_result = NS__QUERY_START_R_AUTH,
+               },
+               /*
+                * Recursive foo/DS query to a server with recursive service
+                * and zone "foo" configured.  Query should be answered from
+                * cache.
+                */
+               {
+                       NS_TEST_ID("foo/DS, RD=1, cache, auth for foo"),
+                       .qname = "foo",
+                       .qtype = dns_rdatatype_ds,
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .recursive_service = ISC_TRUE,
+                       .auth_zone_origin = "foo",
+                       .auth_zone_path = "testdata/query/foo.db",
+                       .expected_result = NS__QUERY_START_R_CACHE,
+               },
+               /*
+                * Non-recursive foo/DS query to a server with recursive
+                * service and zone "foo" configured.  Query should be answered
+                * from authoritative data.
+                */
+               {
+                       NS_TEST_ID("foo/DS, RD=0, cache, auth for foo"),
+                       .qname = "foo",
+                       .qtype = dns_rdatatype_ds,
+                       .qflags = 0,
+                       .recursive_service = ISC_TRUE,
+                       .auth_zone_origin = "foo",
+                       .auth_zone_path = "testdata/query/foo.db",
+                       .expected_result = NS__QUERY_START_R_AUTH,
+               },
+               /*
+                * Recursive _foo/A query to a server with recursive service,
+                * no zones configured and owner name checks disabled.  Query
+                * should be answered from cache.
+                */
+               {
+                       NS_TEST_ID("_foo/A, cache, no auth, name checks off"),
+                       .qname = "_foo",
+                       .qtype = dns_rdatatype_a,
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .disable_name_checks = ISC_TRUE,
+                       .recursive_service = ISC_TRUE,
+                       .expected_result = NS__QUERY_START_R_CACHE,
+               },
+               /*
+                * Recursive _foo/A query to a server with recursive service,
+                * no zones configured and owner name checks enabled.  Query
+                * should be REFUSED.
+                */
+               {
+                       NS_TEST_ID("_foo/A, cache, no auth, name checks on"),
+                       .qname = "_foo",
+                       .qtype = dns_rdatatype_a,
+                       .qflags = DNS_MESSAGEFLAG_RD,
+                       .disable_name_checks = ISC_FALSE,
+                       .recursive_service = ISC_TRUE,
+                       .expected_result = NS__QUERY_START_R_REFUSE,
+               },
+       };
+
+       UNUSED(tc);
+
+       result = ns_test_begin(NULL, ISC_TRUE);
+       ATF_REQUIRE_EQ(result, ISC_R_SUCCESS);
+
+       for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
+               ns__query_start_test(&tests[i]);
+       }
+
+       ns_test_end();
+}
+
+/*
+ * Main
+ */
+ATF_TP_ADD_TCS(tp) {
+       ATF_TP_ADD_TC(tp, ns__query_sfcache);
+       ATF_TP_ADD_TC(tp, ns__query_start);
+
+       return (atf_no_error());
+}
diff --git a/lib/ns/tests/testdata/query/foo.db b/lib/ns/tests/testdata/query/foo.db
new file mode 100644 (file)
index 0000000..20042a6
--- /dev/null
@@ -0,0 +1,15 @@
+; Copyright (C) 2017  Internet Systems Consortium, Inc. ("ISC")
+;
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0. If a copy of the MPL was not distributed with this
+; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+$TTL 3600
+@              IN      SOA     localhost. postmaster.localhost. (
+                               1               ;serial
+                               3600            ;refresh
+                               1800            ;retry
+                               604800          ;expiration
+                               3600 )          ;minimum
+               IN      NS      ns
+ns             IN      A       127.0.0.1