/*	$NetBSD: query_test.c,v 1.3 2025/01/26 16:25:51 christos Exp $	*/

/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * SPDX-License-Identifier: MPL-2.0
 *
 * 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 https://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

#include <inttypes.h>
#include <sched.h> /* IWYU pragma: keep */
#include <setjmp.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

#define UNIT_TESTING
#include <cmocka.h>

#include <isc/quota.h>
#include <isc/util.h>

#include <dns/badcache.h>
#include <dns/view.h>
#include <dns/zone.h>

#include <ns/client.h>
#include <ns/hooks.h>
#include <ns/query.h>
#include <ns/server.h>
#include <ns/stats.h>

#include <tests/ns.h>

/* can be used for client->sendcb to avoid disruption on sending a response */
static void
send_noop(isc_buffer_t *buffer) {
	UNUSED(buffer);
}

/*****
 ***** 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 */
	bool cache_entry_present;   /* whether a SERVFAIL
				     * cache entry
				     * matching the query
				     * should be
				     * present */
	uint32_t cache_entry_flags; /* NS_FAILCACHE_* flags to
				     * set for
				     * the SERVFAIL cache entry
				     * */
	bool 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
run_sfcache_test(const ns__query_sfcache_test_params_t *test) {
	ns_hooktable_t *query_hooks = NULL;
	query_ctx_t *qctx = NULL;
	isc_result_t result;
	const ns_hook_t hook = {
		.action = ns_test_hook_catch_call,
	};

	REQUIRE(test != NULL);
	REQUIRE(test->id.description != NULL);
	REQUIRE(test->cache_entry_present || test->cache_entry_flags == 0);

	/*
	 * Interrupt execution if ns_query_done() is called.
	 */

	ns_hooktable_create(mctx, &query_hooks);
	ns_hook_add(query_hooks, mctx, NS_QUERY_DONE_BEGIN, &hook);
	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 = true,
		};

		result = ns_test_qctx_create(&qctx_params, &qctx);
		assert_int_equal(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);
		assert_int_equal(result, ISC_R_SUCCESS);

		dns_badcache_add(qctx->client->view->failcache, dns_rootname,
				 dns_rdatatype_ns, test->cache_entry_flags,
				 isc_time_seconds(&expire));
	}

	/*
	 * Check whether ns__query_sfcache() behaves as expected.
	 */
	ns__query_sfcache(qctx);

	if (test->servfail_expected) {
		if (qctx->result != DNS_R_SERVFAIL) {
			fail_msg("# test \"%s\" on line %d: "
				 "expected SERVFAIL, got %s",
				 test->id.description, test->id.lineno,
				 isc_result_totext(qctx->result));
		}
	} else {
		if (qctx->result != ISC_R_SUCCESS) {
			fail_msg("# 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);
	ns_hooktable_free(mctx, (void **)&query_hooks);
}

/* test ns__query_sfcache() */
ISC_LOOP_TEST_IMPL(ns__query_sfcache) {
	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 = false,
			.servfail_expected = 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 = true,
			.cache_entry_flags = 0,
			.servfail_expected = 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 = true,
			.cache_entry_flags = 0,
			.servfail_expected = 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 = true,
			.cache_entry_flags = NS_FAILCACHE_CD,
			.servfail_expected = 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 = true,
			.cache_entry_flags = NS_FAILCACHE_CD,
			.servfail_expected = 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 = true,
			.cache_entry_flags = 0,
			.servfail_expected = false,
		},
	};

	for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
		run_sfcache_test(&tests[i]);
	}

	isc_loop_teardown(mainloop, shutdown_interfacemgr, NULL);
	isc_loopmgr_shutdown(loopmgr);
}

/*****
***** 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 */
	bool disable_name_checks;     /* if set to true, owner
				       * name checks will
				       * be disabled for the
				       * view created
				       */
	bool recursive_service;	      /* if set to 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
run_start_test(const ns__query_start_test_params_t *test) {
	ns_hooktable_t *query_hooks = NULL;
	query_ctx_t *qctx = NULL;
	isc_result_t result;
	const ns_hook_t hook = {
		.action = ns_test_hook_catch_call,
	};

	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 ns_query_done() is called.
	 */
	ns_hooktable_create(mctx, &query_hooks);
	ns_hook_add(query_hooks, mctx, NS_QUERY_LOOKUP_BEGIN, &hook);
	ns_hook_add(query_hooks, mctx, NS_QUERY_DONE_BEGIN, &hook);
	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);
		assert_int_equal(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);
		assert_int_equal(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:
		if (qctx->result != DNS_R_REFUSED) {
			fail_msg("# test \"%s\" on line %d: "
				 "expected REFUSED, got %s",
				 test->id.description, test->id.lineno,
				 isc_result_totext(qctx->result));
		}
		if (qctx->zone != NULL) {
			fail_msg("# test \"%s\" on line %d: "
				 "no zone was expected to be attached to "
				 "query context, but some was",
				 test->id.description, test->id.lineno);
		}
		if (qctx->db != NULL) {
			fail_msg("# 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:
		if (qctx->result != ISC_R_SUCCESS) {
			fail_msg("# test \"%s\" on line %d: "
				 "expected success, got %s",
				 test->id.description, test->id.lineno,
				 isc_result_totext(qctx->result));
		}
		if (qctx->zone != NULL) {
			fail_msg("# test \"%s\" on line %d: "
				 "no zone was expected to be attached to "
				 "query context, but some was",
				 test->id.description, test->id.lineno);
		}
		if (qctx->db == NULL || qctx->db != qctx->client->view->cachedb)
		{
			fail_msg("# 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:
		if (qctx->result != ISC_R_SUCCESS) {
			fail_msg("# test \"%s\" on line %d: "
				 "expected success, got %s",
				 test->id.description, test->id.lineno,
				 isc_result_totext(qctx->result));
		}
		if (qctx->zone == NULL) {
			fail_msg("# 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);
		}
		if (qctx->db == qctx->client->view->cachedb) {
			fail_msg("# 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:
		fail_msg("# test \"%s\" on line %d has no expected result set",
			 test->id.description, test->id.lineno);
		break;
	default:
		UNREACHABLE();
	}

	/*
	 * Clean up.
	 */
	if (test->auth_zone_path != NULL) {
		ns_test_cleanup_zone();
	}
	ns_test_qctx_destroy(&qctx);
	ns_hooktable_free(mctx, (void **)&query_hooks);
}

/* test ns__query_start() */
ISC_LOOP_TEST_IMPL(ns__query_start) {
	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 = 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 = 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 = true,
			.auth_zone_origin = "foo",
			.auth_zone_path = TESTS_DIR "/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 = false,
			.auth_zone_origin = "foo",
			.auth_zone_path = TESTS_DIR "/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 = true,
			.auth_zone_origin = "foo",
			.auth_zone_path = TESTS_DIR "/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 = true,
			.auth_zone_origin = "foo",
			.auth_zone_path = TESTS_DIR "/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 = true,
			.auth_zone_origin = "foo",
			.auth_zone_path = TESTS_DIR "/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 = true,
			.auth_zone_origin = "foo",
			.auth_zone_path = TESTS_DIR "/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 = true,
			.auth_zone_origin = "foo",
			.auth_zone_path = TESTS_DIR "/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 = true,
			.recursive_service = 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 = false,
			.recursive_service = true,
			.expected_result = NS__QUERY_START_R_REFUSE,
		},
	};

	for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
		run_start_test(&tests[i]);
	}

	isc_loop_teardown(mainloop, shutdown_interfacemgr, NULL);
	isc_loopmgr_shutdown(loopmgr);
}

/*****
***** tests for ns_query_hookasync().
*****/

/*%
 * Structure containing parameters for ns__query_hookasync_test().
 */
typedef struct {
	const ns_test_id_t id;	   /* libns test identifier */
	ns_hookpoint_t hookpoint;  /* hook point specified for resume */
	ns_hookpoint_t hookpoint2; /* expected hook point used after resume */
	ns_hook_action_t action;   /* action for the hook point */
	isc_result_t start_result; /* result of 'runasync' */
	bool quota_ok;		   /* true if recursion quota should be okay */
	bool do_cancel;		   /* true if query should be canceled
				    * in test */
} ns__query_hookasync_test_params_t;

/* Data structure passed from tests to hooks */
typedef struct hookasync_data {
	bool async;		      /* true if in a hook-triggered
				       * asynchronous process */
	bool canceled;		      /* true if the query has been canceled  */
	isc_result_t start_result;    /* result of 'runasync' */
	ns_hook_resume_t *rev;	      /* resume state sent on completion */
	query_ctx_t qctx;	      /* shallow copy of qctx passed to hook */
	ns_hookpoint_t hookpoint;     /* specifies where to resume */
	ns_hookpoint_t lasthookpoint; /* remember the last hook point called */
} hookasync_data_t;

/*
 * 'destroy' callback of hook recursion ctx.
 * The dynamically allocated context will be freed here, thereby proving
 * this is actually called; otherwise tests would fail due to memory leak.
 */
static void
destroy_hookactx(ns_hookasync_t **ctxp) {
	ns_hookasync_t *ctx = *ctxp;

	*ctxp = NULL;
	isc_mem_putanddetach(&ctx->mctx, ctx, sizeof(*ctx));
}

/* 'cancel' callback of hook recursion ctx. */
static void
cancel_hookactx(ns_hookasync_t *ctx) {
	/* Mark the hook data so the test can confirm this is called. */
	((hookasync_data_t *)ctx->private)->canceled = true;
}

/* 'runasync' callback passed to ns_query_hookasync */
static isc_result_t
test_hookasync(query_ctx_t *qctx, isc_mem_t *memctx, void *arg,
	       isc_loop_t *loop, isc_job_cb cb, void *evarg,
	       ns_hookasync_t **ctxp) {
	hookasync_data_t *asdata = arg;
	ns_hookasync_t *ctx = NULL;
	ns_hook_resume_t *rev = NULL;

	if (asdata->start_result != ISC_R_SUCCESS) {
		return asdata->start_result;
	}

	ctx = isc_mem_get(memctx, sizeof(*ctx));
	rev = isc_mem_get(memctx, sizeof(*rev));
	*rev = (ns_hook_resume_t){
		.hookpoint = asdata->hookpoint,
		.origresult = DNS_R_NXDOMAIN,
		.saved_qctx = qctx,
		.ctx = ctx,
		.loop = loop,
		.cb = cb,
		.arg = evarg,
	};

	asdata->rev = rev;

	*ctx = (ns_hookasync_t){
		.destroy = destroy_hookactx,
		.cancel = cancel_hookactx,
		.private = asdata,
	};
	isc_mem_attach(memctx, &ctx->mctx);

	*ctxp = ctx;
	return ISC_R_SUCCESS;
}

/*
 * Main logic for hook actions.
 * 'hookpoint' should identify the point that calls the hook.  It will be
 * remembered in the hook data, so that the test can confirm which hook point
 * was last used.
 */
static ns_hookresult_t
hook_async_common(void *arg, void *data, isc_result_t *resultp,
		  ns_hookpoint_t hookpoint) {
	query_ctx_t *qctx = arg;
	hookasync_data_t *asdata = data;
	isc_result_t result;

	asdata->qctx = *qctx; /* remember passed ctx for inspection */
	asdata->lasthookpoint = hookpoint; /* ditto */

	if (!asdata->async) {
		/* Initial call to the hook; start recursion */
		result = ns_query_hookasync(qctx, test_hookasync, asdata);
		if (result == ISC_R_SUCCESS) {
			asdata->async = true;
		}
	} else {
		/*
		 * Resume from the completion of async event.  The fetch handle
		 * should have been detached so that we can start another async
		 * event or DNS recursive resolution.
		 */
		INSIST(HANDLE_RECTYPE_HOOK(qctx->client) == NULL);
		asdata->async = false;
		switch (hookpoint) {
		case NS_QUERY_GOT_ANSWER_BEGIN:
		case NS_QUERY_NODATA_BEGIN:
		case NS_QUERY_NXDOMAIN_BEGIN:
		case NS_QUERY_NCACHE_BEGIN:
			INSIST(*resultp == DNS_R_NXDOMAIN);
			break;
		default:;
		}
	}

	*resultp = ISC_R_UNSET;
	return NS_HOOK_RETURN;
}

static ns_hookresult_t
hook_async_query_setup(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_SETUP);
}

static ns_hookresult_t
hook_async_query_start_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_START_BEGIN);
}

static ns_hookresult_t
hook_async_query_lookup_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_LOOKUP_BEGIN);
}

static ns_hookresult_t
hook_async_query_resume_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_RESUME_BEGIN);
}

static ns_hookresult_t
hook_async_query_got_answer_begin(void *arg, void *data,
				  isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_GOT_ANSWER_BEGIN);
}

static ns_hookresult_t
hook_async_query_respond_any_begin(void *arg, void *data,
				   isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp,
				 NS_QUERY_RESPOND_ANY_BEGIN);
}

static ns_hookresult_t
hook_async_query_addanswer_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_ADDANSWER_BEGIN);
}

static ns_hookresult_t
hook_async_query_notfound_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_NOTFOUND_BEGIN);
}

static ns_hookresult_t
hook_async_query_prep_delegation_begin(void *arg, void *data,
				       isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp,
				 NS_QUERY_PREP_DELEGATION_BEGIN);
}

static ns_hookresult_t
hook_async_query_zone_delegation_begin(void *arg, void *data,
				       isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp,
				 NS_QUERY_ZONE_DELEGATION_BEGIN);
}

static ns_hookresult_t
hook_async_query_delegation_begin(void *arg, void *data,
				  isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_DELEGATION_BEGIN);
}

static ns_hookresult_t
hook_async_query_delegation_recurse_begin(void *arg, void *data,
					  isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp,
				 NS_QUERY_DELEGATION_RECURSE_BEGIN);
}

static ns_hookresult_t
hook_async_query_nodata_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_NODATA_BEGIN);
}

static ns_hookresult_t
hook_async_query_nxdomain_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_NXDOMAIN_BEGIN);
}

static ns_hookresult_t
hook_async_query_ncache_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_NCACHE_BEGIN);
}

static ns_hookresult_t
hook_async_query_cname_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_CNAME_BEGIN);
}

static ns_hookresult_t
hook_async_query_dname_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_DNAME_BEGIN);
}

static ns_hookresult_t
hook_async_query_respond_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_RESPOND_BEGIN);
}

static ns_hookresult_t
hook_async_query_response_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp,
				 NS_QUERY_PREP_RESPONSE_BEGIN);
}

static ns_hookresult_t
hook_async_query_done_begin(void *arg, void *data, isc_result_t *resultp) {
	return hook_async_common(arg, data, resultp, NS_QUERY_DONE_BEGIN);
}

/*
 * hook on destroying actx.  Can't be used for async event, but we use this
 * to remember the qctx at that point.
 */
static ns_hookresult_t
ns_test_qctx_destroy_hook(void *arg, void *data, isc_result_t *resultp) {
	query_ctx_t *qctx = arg;
	hookasync_data_t *asdata = data;

	asdata->qctx = *qctx; /* remember passed ctx for inspection */
	*resultp = ISC_R_UNSET;
	return NS_HOOK_CONTINUE;
}

static void
run_hookasync_test(const ns__query_hookasync_test_params_t *test) {
	query_ctx_t *qctx = NULL;
	isc_result_t result;
	hookasync_data_t asdata = {
		.async = false,
		.canceled = false,
		.start_result = test->start_result,
		.hookpoint = test->hookpoint,
	};
	const ns_hook_t testhook = {
		.action = test->action,
		.action_data = &asdata,
	};
	const ns_hook_t destroyhook = {
		.action = ns_test_qctx_destroy_hook,
		.action_data = &asdata,
	};
	isc_statscounter_t srvfail_cnt;
	bool expect_servfail = false;

	/*
	 * Prepare hooks.  We always begin with ns__query_start for simplicity.
	 * Its action will specify various different resume points (unusual
	 * in practice, but that's fine for the testing purpose).
	 */
	ns__hook_table = NULL;
	ns_hooktable_create(mctx, &ns__hook_table);
	ns_hook_add(ns__hook_table, mctx, NS_QUERY_START_BEGIN, &testhook);
	if (test->hookpoint2 != NS_QUERY_START_BEGIN) {
		/*
		 * unless testing START_BEGIN itself, specify the hook for the
		 * expected resume point, too.
		 */
		ns_hook_add(ns__hook_table, mctx, test->hookpoint2, &testhook);
	}
	ns_hook_add(ns__hook_table, mctx, NS_QUERY_QCTX_DESTROYED,
		    &destroyhook);

	{
		const ns_test_qctx_create_params_t qctx_params = {
			.qname = "test.example.com",
			.qtype = dns_rdatatype_aaaa,
		};
		result = ns_test_qctx_create(&qctx_params, &qctx);
		INSIST(result == ISC_R_SUCCESS);
		qctx->client->sendcb = send_noop;
	}

	/*
	 * Set recursion quota to the lowest possible value, then make it full
	 * if we want to exercise a quota failure case.
	 */
	isc_quota_max(&sctx->recursionquota, 1);
	if (!test->quota_ok) {
		result = isc_quota_acquire(&sctx->recursionquota);
		INSIST(result == ISC_R_SUCCESS);
	}

	/* Remember SERVFAIL counter */
	srvfail_cnt = ns_stats_get_counter(qctx->client->manager->sctx->nsstats,
					   ns_statscounter_servfail);

	/*
	 * If the query has been canceled, or async event didn't succeed,
	 * SERVFAIL will have to be sent.  In this case we need to have
	 * 'reqhandle' attach to the client's handle as it's detached in
	 * query_error.
	 */
	if (test->start_result != ISC_R_SUCCESS || !test->quota_ok ||
	    test->do_cancel)
	{
		expect_servfail = true;
		isc_nmhandle_attach(qctx->client->handle,
				    &qctx->client->reqhandle);
	}

	/*
	 * Emulate query handling from query_start.
	 * Specified hook should be called.
	 */
	qctx->client->state = NS_CLIENTSTATE_WORKING;
	result = ns__query_start(qctx);
	INSIST(result == ISC_R_UNSET);

	/*
	 * hook-triggered async event should be happening unless it hits
	 * recursion quota limit or 'runasync' callback fails.
	 */
	INSIST(asdata.async ==
	       (test->quota_ok && test->start_result == ISC_R_SUCCESS));

	/*
	 * Emulate cancel if so specified.
	 * The cancel callback should be called.
	 */
	if (test->do_cancel) {
		ns_query_cancel(qctx->client);
	}
	INSIST(asdata.canceled == test->do_cancel);

	/* If async event has started, manually invoke the 'done' event. */
	if (asdata.async) {
		qctx->client->now = 0; /* set to sentinel before resume */
		asdata.rev->cb(asdata.rev);

		/* Confirm necessary cleanup has been performed. */
		INSIST(qctx->client->query.hookactx == NULL);
		INSIST(qctx->client->state == NS_CLIENTSTATE_WORKING);
		INSIST(ns_stats_get_counter(
			       qctx->client->manager->sctx->nsstats,
			       ns_statscounter_recursclients) == 0);
		INSIST(!ISC_LINK_LINKED(qctx->client, rlink));
		if (!test->do_cancel) {
			/*
			 * In the normal case the client's timestamp is updated
			 * and the query handling has been resumed from the
			 * expected point.
			 */
			INSIST(qctx->client->now != 0);
			INSIST(asdata.lasthookpoint == test->hookpoint2);
		}
	} else {
		INSIST(qctx->client->query.hookactx == NULL);
	}

	/*
	 * Confirm SERVFAIL has been sent if it was expected.
	 * Also, the last-generated qctx should have detach_client being true.
	 */
	if (expect_servfail) {
		INSIST(ns_stats_get_counter(
			       qctx->client->manager->sctx->nsstats,
			       ns_statscounter_servfail) == srvfail_cnt + 1);
		if (test->do_cancel) {
			/* qctx was created on resume and copied in hook */
			INSIST(asdata.qctx.detach_client);
		} else {
			INSIST(qctx->detach_client);
		}
	}

	/*
	 * Cleanup.  Note that we've kept 'qctx' until now; otherwise
	 * qctx->client may have been invalidated while we still need it.
	 */
	ns_test_qctx_destroy(&qctx);
	ns_hooktable_free(mctx, (void **)&ns__hook_table);
	if (!test->quota_ok) {
		isc_quota_release(&sctx->recursionquota);
	}
}

ISC_LOOP_TEST_IMPL(ns__query_hookasync) {
	size_t i;

	const ns__query_hookasync_test_params_t tests[] = {
		{
			NS_TEST_ID("normal case"),
			NS_QUERY_START_BEGIN,
			NS_QUERY_START_BEGIN,
			hook_async_query_start_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("quota fail"),
			NS_QUERY_START_BEGIN,
			NS_QUERY_START_BEGIN,
			hook_async_query_start_begin,
			ISC_R_SUCCESS,
			false,
			false,
		},
		{
			NS_TEST_ID("start fail"),
			NS_QUERY_START_BEGIN,
			NS_QUERY_START_BEGIN,
			hook_async_query_start_begin,
			ISC_R_FAILURE,
			true,
			false,
		},
		{
			NS_TEST_ID("query cancel"),
			NS_QUERY_START_BEGIN,
			NS_QUERY_START_BEGIN,
			hook_async_query_start_begin,
			ISC_R_SUCCESS,
			true,
			true,
		},
		/*
		 * The rest of the test case just confirms supported hookpoints
		 * with the same test logic.
		 */
		{
			NS_TEST_ID("async from setup"),
			NS_QUERY_SETUP,
			NS_QUERY_SETUP,
			hook_async_query_setup,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from lookup"),
			NS_QUERY_LOOKUP_BEGIN,
			NS_QUERY_LOOKUP_BEGIN,
			hook_async_query_lookup_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from resume"),
			NS_QUERY_RESUME_BEGIN,
			NS_QUERY_RESUME_BEGIN,
			hook_async_query_resume_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from resume restored"),
			NS_QUERY_RESUME_RESTORED,
			NS_QUERY_RESUME_BEGIN,
			hook_async_query_resume_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from gotanswer"),
			NS_QUERY_GOT_ANSWER_BEGIN,
			NS_QUERY_GOT_ANSWER_BEGIN,
			hook_async_query_got_answer_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from respond any"),
			NS_QUERY_RESPOND_ANY_BEGIN,
			NS_QUERY_RESPOND_ANY_BEGIN,
			hook_async_query_respond_any_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from add answer"),
			NS_QUERY_ADDANSWER_BEGIN,
			NS_QUERY_ADDANSWER_BEGIN,
			hook_async_query_addanswer_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from notfound"),
			NS_QUERY_NOTFOUND_BEGIN,
			NS_QUERY_NOTFOUND_BEGIN,
			hook_async_query_notfound_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from prep delegation"),
			NS_QUERY_PREP_DELEGATION_BEGIN,
			NS_QUERY_PREP_DELEGATION_BEGIN,
			hook_async_query_prep_delegation_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from zone delegation"),
			NS_QUERY_ZONE_DELEGATION_BEGIN,
			NS_QUERY_ZONE_DELEGATION_BEGIN,
			hook_async_query_zone_delegation_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from delegation"),
			NS_QUERY_DELEGATION_BEGIN,
			NS_QUERY_DELEGATION_BEGIN,
			hook_async_query_delegation_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from async delegation"),
			NS_QUERY_DELEGATION_RECURSE_BEGIN,
			NS_QUERY_DELEGATION_RECURSE_BEGIN,
			hook_async_query_delegation_recurse_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from nodata"),
			NS_QUERY_NODATA_BEGIN,
			NS_QUERY_NODATA_BEGIN,
			hook_async_query_nodata_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from nxdomain"),
			NS_QUERY_NXDOMAIN_BEGIN,
			NS_QUERY_NXDOMAIN_BEGIN,
			hook_async_query_nxdomain_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from ncache"),
			NS_QUERY_NCACHE_BEGIN,
			NS_QUERY_NCACHE_BEGIN,
			hook_async_query_ncache_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from CNAME"),
			NS_QUERY_CNAME_BEGIN,
			NS_QUERY_CNAME_BEGIN,
			hook_async_query_cname_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from DNAME"),
			NS_QUERY_DNAME_BEGIN,
			NS_QUERY_DNAME_BEGIN,
			hook_async_query_dname_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from prep response"),
			NS_QUERY_PREP_RESPONSE_BEGIN,
			NS_QUERY_PREP_RESPONSE_BEGIN,
			hook_async_query_response_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from respond"),
			NS_QUERY_RESPOND_BEGIN,
			NS_QUERY_RESPOND_BEGIN,
			hook_async_query_respond_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from done begin"),
			NS_QUERY_DONE_BEGIN,
			NS_QUERY_DONE_BEGIN,
			hook_async_query_done_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
		{
			NS_TEST_ID("async from done send"),
			NS_QUERY_DONE_SEND,
			NS_QUERY_DONE_BEGIN,
			hook_async_query_done_begin,
			ISC_R_SUCCESS,
			true,
			false,
		},
	};

	for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
		run_hookasync_test(&tests[i]);
	}

	isc_loop_teardown(mainloop, shutdown_interfacemgr, NULL);
	isc_loopmgr_shutdown(loopmgr);
}

/*****
***** tests for higher level ("e2e") behavior of ns_query_hookasync().
***** It exercises overall behavior for some selected cases, while
***** ns__query_hookasync_test exercises implementation details for a
***** simple scenario and for all supported hook points.
*****/

/*%
 * Structure containing parameters for ns__query_hookasync_e2e_test().
 */
typedef struct {
	const ns_test_id_t id;	   /* libns test identifier */
	const char *qname;	   /* QNAME */
	ns_hookpoint_t hookpoint;  /* hook point specified for resume */
	isc_result_t start_result; /* result of 'runasync' */
	bool do_cancel;		   /* true if query should be canceled
				    * in test */
	dns_rcode_t expected_rcode;
} ns__query_hookasync_e2e_test_params_t;

/* data structure passed from tests to hooks */
typedef struct hookasync_e2e_data {
	bool async;		   /* true if in a hook-triggered
				    * asynchronous process */
	ns_hook_resume_t *rev;	   /* resume state sent on completion */
	ns_hookpoint_t hookpoint;  /* specifies where to resume */
	isc_result_t start_result; /* result of 'runasync' */
	dns_rcode_t expected_rcode;
	bool done; /* if SEND_DONE hook is called */
} hookasync_e2e_data_t;

/* Cancel callback.  Just need to be defined, it doesn't have to do anything. */
static void
cancel_e2ehookactx(ns_hookasync_t *ctx) {
	UNUSED(ctx);
}

/* 'runasync' callback passed to ns_query_hookasync */
static isc_result_t
test_hookasync_e2e(query_ctx_t *qctx, isc_mem_t *memctx, void *arg,
		   isc_loop_t *loop, isc_job_cb cb, void *evarg,
		   ns_hookasync_t **ctxp) {
	ns_hookasync_t *ctx = NULL;
	ns_hook_resume_t *rev = NULL;
	hookasync_e2e_data_t *asdata = arg;

	if (asdata->start_result != ISC_R_SUCCESS) {
		return asdata->start_result;
	}

	ctx = isc_mem_get(memctx, sizeof(*ctx));
	rev = isc_mem_get(memctx, sizeof(*rev));
	*rev = (ns_hook_resume_t){
		.hookpoint = asdata->hookpoint,
		.saved_qctx = qctx,
		.ctx = ctx,
		.loop = loop,
		.cb = cb,
		.arg = evarg,
	};

	asdata->rev = rev;

	*ctx = (ns_hookasync_t){
		.destroy = destroy_hookactx,
		.cancel = cancel_e2ehookactx,
		.private = asdata,
	};
	isc_mem_attach(memctx, &ctx->mctx);

	*ctxp = ctx;
	return ISC_R_SUCCESS;
}

static ns_hookresult_t
hook_async_e2e(void *arg, void *data, isc_result_t *resultp) {
	query_ctx_t *qctx = arg;
	hookasync_e2e_data_t *asdata = data;
	isc_result_t result;

	if (!asdata->async) {
		/* Initial call to the hook; start async event */
		result = ns_query_hookasync(qctx, test_hookasync_e2e, asdata);
		if (result != ISC_R_SUCCESS) {
			*resultp = result;
			return NS_HOOK_RETURN;
		}

		asdata->async = true;
		asdata->rev->origresult = *resultp; /* save it for resume */
		*resultp = ISC_R_UNSET;
		return NS_HOOK_RETURN;
	} else {
		/* Resume from the completion of async event */
		asdata->async = false;
		/* Don't touch 'resultp' */
		return NS_HOOK_CONTINUE;
	}
}

/*
 * Check whether the final response has expected the RCODE according to
 * the test scenario.
 */
static ns_hookresult_t
hook_donesend(void *arg, void *data, isc_result_t *resultp) {
	query_ctx_t *qctx = arg;
	hookasync_e2e_data_t *asdata = data;

	INSIST(qctx->client->message->rcode == asdata->expected_rcode);
	asdata->done = true; /* Let the test know this hook is called */
	*resultp = ISC_R_UNSET;
	return NS_HOOK_CONTINUE;
}

static void
run_hookasync_e2e_test(const ns__query_hookasync_e2e_test_params_t *test) {
	query_ctx_t *qctx = NULL;
	isc_result_t result;
	hookasync_e2e_data_t asdata = {
		.async = false,
		.hookpoint = test->hookpoint,
		.start_result = test->start_result,
		.expected_rcode = test->expected_rcode,
		.done = false,
	};
	const ns_hook_t donesend_hook = {
		.action = hook_donesend,
		.action_data = &asdata,
	};
	const ns_hook_t hook = {
		.action = hook_async_e2e,
		.action_data = &asdata,
	};
	const ns_test_qctx_create_params_t qctx_params = {
		.qname = test->qname,
		.qtype = dns_rdatatype_a,
		.with_cache = true,
	};

	ns__hook_table = NULL;
	ns_hooktable_create(mctx, &ns__hook_table);
	ns_hook_add(ns__hook_table, mctx, test->hookpoint, &hook);
	ns_hook_add(ns__hook_table, mctx, NS_QUERY_DONE_SEND, &donesend_hook);

	result = ns_test_qctx_create(&qctx_params, &qctx);
	INSIST(result == ISC_R_SUCCESS);

	isc_sockaddr_any(&qctx->client->peeraddr); /* for sortlist */
	qctx->client->sendcb = send_noop;

	/* Load a zone.  it should have ns.foo/A */
	result = ns_test_serve_zone("foo", TESTS_DIR "/testdata/query/foo.db",
				    qctx->client->view);
	INSIST(result == ISC_R_SUCCESS);

	/*
	 * We expect to have a response sent all cases, so we need to
	 * setup reqhandle (which will be detached on the send).
	 */
	isc_nmhandle_attach(qctx->client->handle, &qctx->client->reqhandle);

	/* Handle the query.  hook-based async event will be triggered. */
	qctx->client->state = NS_CLIENTSTATE_WORKING;
	ns__query_start(qctx);

	/* If specified cancel the query at this point. */
	if (test->do_cancel) {
		ns_query_cancel(qctx->client);
	}

	if (test->start_result == ISC_R_SUCCESS) {
		/*
		 * If async event has started, manually invoke the done event.
		 */
		INSIST(asdata.async);
		asdata.rev->cb(asdata.rev);

		/*
		 * Usually 'async' is reset to false on the 2nd call to
		 * the hook.  But the hook isn't called if the query is
		 * canceled.
		 */
		INSIST(asdata.done == !test->do_cancel);
		INSIST(asdata.async == test->do_cancel);
	} else {
		INSIST(!asdata.async);
	}

	/* Cleanup */
	ns_test_qctx_destroy(&qctx);
	ns_test_cleanup_zone();
	ns_hooktable_free(mctx, (void **)&ns__hook_table);
}

ISC_LOOP_TEST_IMPL(ns__query_hookasync_e2e) {
	const ns__query_hookasync_e2e_test_params_t tests[] = {
		{
			NS_TEST_ID("positive answer"),
			"ns.foo",
			NS_QUERY_GOT_ANSWER_BEGIN,
			ISC_R_SUCCESS,
			false,
			dns_rcode_noerror,
		},
		{
			NS_TEST_ID("NXDOMAIN"),
			"notexist.foo",
			NS_QUERY_NXDOMAIN_BEGIN,
			ISC_R_SUCCESS,
			false,
			dns_rcode_nxdomain,
		},
		{
			NS_TEST_ID("async fail"),
			"ns.foo",
			NS_QUERY_DONE_BEGIN,
			ISC_R_FAILURE,
			false,
			-1,
		},
		{
			NS_TEST_ID("cancel query"),
			"ns.foo",
			NS_QUERY_DONE_BEGIN,
			ISC_R_SUCCESS,
			true,
			-1,
		},
	};

	for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
		run_hookasync_e2e_test(&tests[i]);
	}

	isc_loop_teardown(mainloop, shutdown_interfacemgr, NULL);
	isc_loopmgr_shutdown(loopmgr);
}

ISC_TEST_LIST_START
ISC_TEST_ENTRY_CUSTOM(ns__query_sfcache, setup_server, teardown_server)
ISC_TEST_ENTRY_CUSTOM(ns__query_start, setup_server, teardown_server)
ISC_TEST_ENTRY_CUSTOM(ns__query_hookasync, setup_server, teardown_server)
ISC_TEST_ENTRY_CUSTOM(ns__query_hookasync_e2e, setup_server, teardown_server)
ISC_TEST_LIST_END

ISC_TEST_MAIN
