1/*	$NetBSD: query_test.c,v 1.2 2024/02/21 22:52:51 christos Exp $	*/
2
3/*
4 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
5 *
6 * SPDX-License-Identifier: MPL-2.0
7 *
8 * This Source Code Form is subject to the terms of the Mozilla Public
9 * License, v. 2.0. If a copy of the MPL was not distributed with this
10 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
11 *
12 * See the COPYRIGHT file distributed with this work for additional
13 * information regarding copyright ownership.
14 */
15
16#include <inttypes.h>
17#include <sched.h> /* IWYU pragma: keep */
18#include <setjmp.h>
19#include <stdarg.h>
20#include <stdbool.h>
21#include <stddef.h>
22#include <stdlib.h>
23#include <string.h>
24
25#define UNIT_TESTING
26#include <cmocka.h>
27
28#include <isc/quota.h>
29#include <isc/util.h>
30
31#include <dns/badcache.h>
32#include <dns/view.h>
33#include <dns/zone.h>
34
35#include <ns/client.h>
36#include <ns/events.h>
37#include <ns/hooks.h>
38#include <ns/query.h>
39#include <ns/server.h>
40#include <ns/stats.h>
41
42#include <tests/ns.h>
43
44static int
45setup_test(void **state) {
46	isc__nm_force_tid(0);
47	setup_server(state);
48	return (0);
49}
50
51static int
52teardown_test(void **state) {
53	isc__nm_force_tid(-1);
54	teardown_server(state);
55	return (0);
56}
57
58/* can be used for client->sendcb to avoid disruption on sending a response */
59static void
60send_noop(isc_buffer_t *buffer) {
61	UNUSED(buffer);
62}
63
64/*****
65***** ns__query_sfcache() tests
66*****/
67
68/*%
69 * Structure containing parameters for ns__query_sfcache_test().
70 */
71typedef struct {
72	const ns_test_id_t id;	    /* libns test identifier */
73	unsigned int qflags;	    /* query flags */
74	bool cache_entry_present;   /* whether a SERVFAIL
75				     * cache entry
76				     * matching the query
77				     * should be
78				     * present */
79	uint32_t cache_entry_flags; /* NS_FAILCACHE_* flags to
80				     * set for
81				     * the SERVFAIL cache entry
82				     * */
83	bool servfail_expected;	    /* whether a cached
84				     * SERVFAIL is
85				     * expected to be returned
86				     * */
87} ns__query_sfcache_test_params_t;
88
89/*%
90 * Perform a single ns__query_sfcache() check using given parameters.
91 */
92static void
93run_sfcache_test(const ns__query_sfcache_test_params_t *test) {
94	ns_hooktable_t *query_hooks = NULL;
95	query_ctx_t *qctx = NULL;
96	isc_result_t result;
97	const ns_hook_t hook = {
98		.action = ns_test_hook_catch_call,
99	};
100
101	REQUIRE(test != NULL);
102	REQUIRE(test->id.description != NULL);
103	REQUIRE(test->cache_entry_present || test->cache_entry_flags == 0);
104
105	/*
106	 * Interrupt execution if ns_query_done() is called.
107	 */
108
109	ns_hooktable_create(mctx, &query_hooks);
110	ns_hook_add(query_hooks, mctx, NS_QUERY_DONE_BEGIN, &hook);
111	ns__hook_table = query_hooks;
112
113	/*
114	 * Construct a query context for a ./NS query with given flags.
115	 */
116	{
117		const ns_test_qctx_create_params_t qctx_params = {
118			.qname = ".",
119			.qtype = dns_rdatatype_ns,
120			.qflags = test->qflags,
121			.with_cache = true,
122		};
123
124		result = ns_test_qctx_create(&qctx_params, &qctx);
125		assert_int_equal(result, ISC_R_SUCCESS);
126	}
127
128	/*
129	 * If this test wants a SERVFAIL cache entry matching the query to
130	 * exist, create it.
131	 */
132	if (test->cache_entry_present) {
133		isc_interval_t hour;
134		isc_time_t expire;
135
136		isc_interval_set(&hour, 3600, 0);
137		result = isc_time_nowplusinterval(&expire, &hour);
138		assert_int_equal(result, ISC_R_SUCCESS);
139
140		dns_badcache_add(qctx->client->view->failcache, dns_rootname,
141				 dns_rdatatype_ns, true,
142				 test->cache_entry_flags, &expire);
143	}
144
145	/*
146	 * Check whether ns__query_sfcache() behaves as expected.
147	 */
148	ns__query_sfcache(qctx);
149
150	if (test->servfail_expected) {
151		if (qctx->result != DNS_R_SERVFAIL) {
152			fail_msg("# test \"%s\" on line %d: "
153				 "expected SERVFAIL, got %s",
154				 test->id.description, test->id.lineno,
155				 isc_result_totext(qctx->result));
156		}
157	} else {
158		if (qctx->result != ISC_R_SUCCESS) {
159			fail_msg("# test \"%s\" on line %d: "
160				 "expected success, got %s",
161				 test->id.description, test->id.lineno,
162				 isc_result_totext(qctx->result));
163		}
164	}
165
166	/*
167	 * Clean up.
168	 */
169	ns_test_qctx_destroy(&qctx);
170	ns_hooktable_free(mctx, (void **)&query_hooks);
171}
172
173/* test ns__query_sfcache() */
174ISC_RUN_TEST_IMPL(ns_query_sfcache) {
175	size_t i;
176
177	const ns__query_sfcache_test_params_t tests[] = {
178		/*
179		 * Sanity check for an empty SERVFAIL cache.
180		 */
181		{
182			NS_TEST_ID("query: RD=1, CD=0; cache: empty"),
183			.qflags = DNS_MESSAGEFLAG_RD,
184			.cache_entry_present = false,
185			.servfail_expected = false,
186		},
187		/*
188		 * Query: RD=1, CD=0.  Cache entry: CD=0.  Should SERVFAIL.
189		 */
190		{
191			NS_TEST_ID("query: RD=1, CD=0; cache: CD=0"),
192			.qflags = DNS_MESSAGEFLAG_RD,
193			.cache_entry_present = true,
194			.cache_entry_flags = 0,
195			.servfail_expected = true,
196		},
197		/*
198		 * Query: RD=1, CD=1.  Cache entry: CD=0.  Should not SERVFAIL:
199		 * failed validation should not influence CD=1 queries.
200		 */
201		{
202			NS_TEST_ID("query: RD=1, CD=1; cache: CD=0"),
203			.qflags = DNS_MESSAGEFLAG_RD | DNS_MESSAGEFLAG_CD,
204			.cache_entry_present = true,
205			.cache_entry_flags = 0,
206			.servfail_expected = false,
207		},
208		/*
209		 * Query: RD=1, CD=1.  Cache entry: CD=1.  Should SERVFAIL:
210		 * SERVFAIL responses elicited by CD=1 queries can be
211		 * "replayed" for other CD=1 queries during the lifetime of the
212		 * SERVFAIL cache entry.
213		 */
214		{
215			NS_TEST_ID("query: RD=1, CD=1; cache: CD=1"),
216			.qflags = DNS_MESSAGEFLAG_RD | DNS_MESSAGEFLAG_CD,
217			.cache_entry_present = true,
218			.cache_entry_flags = NS_FAILCACHE_CD,
219			.servfail_expected = true,
220		},
221		/*
222		 * Query: RD=1, CD=0.  Cache entry: CD=1.  Should SERVFAIL: if
223		 * a CD=1 query elicited a SERVFAIL, a CD=0 query for the same
224		 * QNAME and QTYPE will SERVFAIL as well.
225		 */
226		{
227			NS_TEST_ID("query: RD=1, CD=0; cache: CD=1"),
228			.qflags = DNS_MESSAGEFLAG_RD,
229			.cache_entry_present = true,
230			.cache_entry_flags = NS_FAILCACHE_CD,
231			.servfail_expected = true,
232		},
233		/*
234		 * Query: RD=0, CD=0.  Cache entry: CD=0.  Should not SERVFAIL
235		 * despite a matching entry being present as the SERVFAIL cache
236		 * should not be consulted for non-recursive queries.
237		 */
238		{
239			NS_TEST_ID("query: RD=0, CD=0; cache: CD=0"),
240			.qflags = 0,
241			.cache_entry_present = true,
242			.cache_entry_flags = 0,
243			.servfail_expected = false,
244		},
245	};
246
247	UNUSED(state);
248
249	for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
250		run_sfcache_test(&tests[i]);
251	}
252}
253
254/*****
255***** ns__query_start() tests
256*****/
257
258/*%
259 * Structure containing parameters for ns__query_start_test().
260 */
261typedef struct {
262	const ns_test_id_t id;	      /* libns test identifier */
263	const char *qname;	      /* QNAME */
264	dns_rdatatype_t qtype;	      /* QTYPE */
265	unsigned int qflags;	      /* query flags */
266	bool disable_name_checks;     /* if set to true, owner
267				       * name
268				       *          checks will
269				       * be disabled for the
270				       *          view created
271				       * */
272	bool recursive_service;	      /* if set to true, the view
273				       *          created will
274				       * have a cache
275				       *          database
276				       * attached */
277	const char *auth_zone_origin; /* origin name of the zone
278				       * the
279				       * created view will be
280				       * authoritative for */
281	const char *auth_zone_path;   /* path to load the
282				       * authoritative
283				       * zone from */
284	enum {			      /* expected result: */
285	       NS__QUERY_START_R_INVALID,
286	       NS__QUERY_START_R_REFUSE, /* query should be REFUSED */
287	       NS__QUERY_START_R_CACHE,	 /* query should be answered from
288					  * cache */
289	       NS__QUERY_START_R_AUTH,	 /* query should be answered using
290					  * authoritative data */
291	} expected_result;
292} ns__query_start_test_params_t;
293
294/*%
295 * Perform a single ns__query_start() check using given parameters.
296 */
297static void
298run_start_test(const ns__query_start_test_params_t *test) {
299	ns_hooktable_t *query_hooks = NULL;
300	query_ctx_t *qctx = NULL;
301	isc_result_t result;
302	const ns_hook_t hook = {
303		.action = ns_test_hook_catch_call,
304	};
305
306	REQUIRE(test != NULL);
307	REQUIRE(test->id.description != NULL);
308	REQUIRE((test->auth_zone_origin == NULL &&
309		 test->auth_zone_path == NULL) ||
310		(test->auth_zone_origin != NULL &&
311		 test->auth_zone_path != NULL));
312
313	/*
314	 * Interrupt execution if query_lookup() or ns_query_done() is called.
315	 */
316
317	ns_hooktable_create(mctx, &query_hooks);
318	ns_hook_add(query_hooks, mctx, NS_QUERY_LOOKUP_BEGIN, &hook);
319	ns_hook_add(query_hooks, mctx, NS_QUERY_DONE_BEGIN, &hook);
320	ns__hook_table = query_hooks;
321
322	/*
323	 * Construct a query context using the supplied parameters.
324	 */
325	{
326		const ns_test_qctx_create_params_t qctx_params = {
327			.qname = test->qname,
328			.qtype = test->qtype,
329			.qflags = test->qflags,
330			.with_cache = test->recursive_service,
331		};
332		result = ns_test_qctx_create(&qctx_params, &qctx);
333		assert_int_equal(result, ISC_R_SUCCESS);
334	}
335
336	/*
337	 * Enable view->checknames by default, disable if requested.
338	 */
339	qctx->client->view->checknames = !test->disable_name_checks;
340
341	/*
342	 * Load zone from file and attach it to the client's view, if
343	 * requested.
344	 */
345	if (test->auth_zone_path != NULL) {
346		result = ns_test_serve_zone(test->auth_zone_origin,
347					    test->auth_zone_path,
348					    qctx->client->view);
349		assert_int_equal(result, ISC_R_SUCCESS);
350	}
351
352	/*
353	 * Check whether ns__query_start() behaves as expected.
354	 */
355	ns__query_start(qctx);
356
357	switch (test->expected_result) {
358	case NS__QUERY_START_R_REFUSE:
359		if (qctx->result != DNS_R_REFUSED) {
360			fail_msg("# test \"%s\" on line %d: "
361				 "expected REFUSED, got %s",
362				 test->id.description, test->id.lineno,
363				 isc_result_totext(qctx->result));
364		}
365		if (qctx->zone != NULL) {
366			fail_msg("# test \"%s\" on line %d: "
367				 "no zone was expected to be attached to "
368				 "query context, but some was",
369				 test->id.description, test->id.lineno);
370		}
371		if (qctx->db != NULL) {
372			fail_msg("# test \"%s\" on line %d: "
373				 "no database was expected to be attached to "
374				 "query context, but some was",
375				 test->id.description, test->id.lineno);
376		}
377		break;
378	case NS__QUERY_START_R_CACHE:
379		if (qctx->result != ISC_R_SUCCESS) {
380			fail_msg("# test \"%s\" on line %d: "
381				 "expected success, got %s",
382				 test->id.description, test->id.lineno,
383				 isc_result_totext(qctx->result));
384		}
385		if (qctx->zone != NULL) {
386			fail_msg("# test \"%s\" on line %d: "
387				 "no zone was expected to be attached to "
388				 "query context, but some was",
389				 test->id.description, test->id.lineno);
390		}
391		if (qctx->db == NULL || qctx->db != qctx->client->view->cachedb)
392		{
393			fail_msg("# test \"%s\" on line %d: "
394				 "cache database was expected to be "
395				 "attached to query context, but it was not",
396				 test->id.description, test->id.lineno);
397		}
398		break;
399	case NS__QUERY_START_R_AUTH:
400		if (qctx->result != ISC_R_SUCCESS) {
401			fail_msg("# test \"%s\" on line %d: "
402				 "expected success, got %s",
403				 test->id.description, test->id.lineno,
404				 isc_result_totext(qctx->result));
405		}
406		if (qctx->zone == NULL) {
407			fail_msg("# test \"%s\" on line %d: "
408				 "a zone was expected to be attached to query "
409				 "context, but it was not",
410				 test->id.description, test->id.lineno);
411		}
412		if (qctx->db == qctx->client->view->cachedb) {
413			fail_msg("# test \"%s\" on line %d: "
414				 "cache database was not expected to be "
415				 "attached to query context, but it is",
416				 test->id.description, test->id.lineno);
417		}
418		break;
419	case NS__QUERY_START_R_INVALID:
420		fail_msg("# test \"%s\" on line %d has no expected result set",
421			 test->id.description, test->id.lineno);
422		break;
423	default:
424		UNREACHABLE();
425	}
426
427	/*
428	 * Clean up.
429	 */
430	if (test->auth_zone_path != NULL) {
431		ns_test_cleanup_zone();
432	}
433	ns_test_qctx_destroy(&qctx);
434	ns_hooktable_free(mctx, (void **)&query_hooks);
435}
436
437/* test ns__query_start() */
438ISC_RUN_TEST_IMPL(ns_query_start) {
439	size_t i;
440
441	const ns__query_start_test_params_t tests[] = {
442		/*
443		 * Recursive foo/A query to a server without recursive service
444		 * and no zones configured.  Query should be REFUSED.
445		 */
446		{
447			NS_TEST_ID("foo/A, no cache, no auth"),
448			.qname = "foo",
449			.qtype = dns_rdatatype_a,
450			.qflags = DNS_MESSAGEFLAG_RD,
451			.recursive_service = false,
452			.expected_result = NS__QUERY_START_R_REFUSE,
453		},
454		/*
455		 * Recursive foo/A query to a server with recursive service and
456		 * no zones configured.  Query should be answered from cache.
457		 */
458		{
459			NS_TEST_ID("foo/A, cache, no auth"),
460			.qname = "foo",
461			.qtype = dns_rdatatype_a,
462			.recursive_service = true,
463			.expected_result = NS__QUERY_START_R_CACHE,
464		},
465		/*
466		 * Recursive foo/A query to a server with recursive service and
467		 * zone "foo" configured.  Query should be answered from
468		 * authoritative data.
469		 */
470		{
471			NS_TEST_ID("foo/A, RD=1, cache, auth for foo"),
472			.qname = "foo",
473			.qtype = dns_rdatatype_a,
474			.qflags = DNS_MESSAGEFLAG_RD,
475			.recursive_service = true,
476			.auth_zone_origin = "foo",
477			.auth_zone_path = TESTS_DIR "/testdata/query/foo.db",
478			.expected_result = NS__QUERY_START_R_AUTH,
479		},
480		/*
481		 * Recursive bar/A query to a server without recursive service
482		 * and zone "foo" configured.  Query should be REFUSED.
483		 */
484		{
485			NS_TEST_ID("bar/A, RD=1, no cache, auth for foo"),
486			.qname = "bar",
487			.qtype = dns_rdatatype_a,
488			.qflags = DNS_MESSAGEFLAG_RD,
489			.recursive_service = false,
490			.auth_zone_origin = "foo",
491			.auth_zone_path = TESTS_DIR "/testdata/query/foo.db",
492			.expected_result = NS__QUERY_START_R_REFUSE,
493		},
494		/*
495		 * Recursive bar/A query to a server with recursive service and
496		 * zone "foo" configured.  Query should be answered from
497		 * cache.
498		 */
499		{
500			NS_TEST_ID("bar/A, RD=1, cache, auth for foo"),
501			.qname = "bar",
502			.qtype = dns_rdatatype_a,
503			.qflags = DNS_MESSAGEFLAG_RD,
504			.recursive_service = true,
505			.auth_zone_origin = "foo",
506			.auth_zone_path = TESTS_DIR "/testdata/query/foo.db",
507			.expected_result = NS__QUERY_START_R_CACHE,
508		},
509		/*
510		 * Recursive bar.foo/DS query to a server with recursive
511		 * service and zone "foo" configured.  Query should be answered
512		 * from authoritative data.
513		 */
514		{
515			NS_TEST_ID("bar.foo/DS, RD=1, cache, auth for foo"),
516			.qname = "bar.foo",
517			.qtype = dns_rdatatype_ds,
518			.qflags = DNS_MESSAGEFLAG_RD,
519			.recursive_service = true,
520			.auth_zone_origin = "foo",
521			.auth_zone_path = TESTS_DIR "/testdata/query/foo.db",
522			.expected_result = NS__QUERY_START_R_AUTH,
523		},
524		/*
525		 * Non-recursive bar.foo/DS query to a server with recursive
526		 * service and zone "foo" configured.  Query should be answered
527		 * from authoritative data.
528		 */
529		{
530			NS_TEST_ID("bar.foo/DS, RD=0, cache, auth for foo"),
531			.qname = "bar.foo",
532			.qtype = dns_rdatatype_ds,
533			.qflags = 0,
534			.recursive_service = true,
535			.auth_zone_origin = "foo",
536			.auth_zone_path = TESTS_DIR "/testdata/query/foo.db",
537			.expected_result = NS__QUERY_START_R_AUTH,
538		},
539		/*
540		 * Recursive foo/DS query to a server with recursive service
541		 * and zone "foo" configured.  Query should be answered from
542		 * cache.
543		 */
544		{
545			NS_TEST_ID("foo/DS, RD=1, cache, auth for foo"),
546			.qname = "foo",
547			.qtype = dns_rdatatype_ds,
548			.qflags = DNS_MESSAGEFLAG_RD,
549			.recursive_service = true,
550			.auth_zone_origin = "foo",
551			.auth_zone_path = TESTS_DIR "/testdata/query/foo.db",
552			.expected_result = NS__QUERY_START_R_CACHE,
553		},
554		/*
555		 * Non-recursive foo/DS query to a server with recursive
556		 * service and zone "foo" configured.  Query should be answered
557		 * from authoritative data.
558		 */
559		{
560			NS_TEST_ID("foo/DS, RD=0, cache, auth for foo"),
561			.qname = "foo",
562			.qtype = dns_rdatatype_ds,
563			.qflags = 0,
564			.recursive_service = true,
565			.auth_zone_origin = "foo",
566			.auth_zone_path = TESTS_DIR "/testdata/query/foo.db",
567			.expected_result = NS__QUERY_START_R_AUTH,
568		},
569		/*
570		 * Recursive _foo/A query to a server with recursive service,
571		 * no zones configured and owner name checks disabled.  Query
572		 * should be answered from cache.
573		 */
574		{
575			NS_TEST_ID("_foo/A, cache, no auth, name checks off"),
576			.qname = "_foo",
577			.qtype = dns_rdatatype_a,
578			.qflags = DNS_MESSAGEFLAG_RD,
579			.disable_name_checks = true,
580			.recursive_service = true,
581			.expected_result = NS__QUERY_START_R_CACHE,
582		},
583		/*
584		 * Recursive _foo/A query to a server with recursive service,
585		 * no zones configured and owner name checks enabled.  Query
586		 * should be REFUSED.
587		 */
588		{
589			NS_TEST_ID("_foo/A, cache, no auth, name checks on"),
590			.qname = "_foo",
591			.qtype = dns_rdatatype_a,
592			.qflags = DNS_MESSAGEFLAG_RD,
593			.disable_name_checks = false,
594			.recursive_service = true,
595			.expected_result = NS__QUERY_START_R_REFUSE,
596		},
597	};
598
599	UNUSED(state);
600
601	for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
602		run_start_test(&tests[i]);
603	}
604}
605
606/*****
607***** tests for ns_query_hookasync().
608*****/
609
610/*%
611 * Structure containing parameters for ns__query_hookasync_test().
612 */
613typedef struct {
614	const ns_test_id_t id;	   /* libns test identifier */
615	ns_hookpoint_t hookpoint;  /* hook point specified for resume */
616	ns_hookpoint_t hookpoint2; /* expected hook point used after resume */
617	ns_hook_action_t action;   /* action for the hook point */
618	isc_result_t start_result; /* result of 'runasync' */
619	bool quota_ok;		   /* true if recursion quota should be okay */
620	bool do_cancel;		   /* true if query should be canceled
621				    * in test */
622} ns__query_hookasync_test_params_t;
623
624/* Data structure passed from tests to hooks */
625typedef struct hookasync_data {
626	bool async;		      /* true if in a hook-triggered
627				       * asynchronous process */
628	bool canceled;		      /* true if the query has been canceled  */
629	isc_result_t start_result;    /* result of 'runasync' */
630	ns_hook_resevent_t *rev;      /* resume event sent on completion */
631	query_ctx_t qctx;	      /* shallow copy of qctx passed to hook */
632	ns_hookpoint_t hookpoint;     /* specifies where to resume */
633	ns_hookpoint_t lasthookpoint; /* remember the last hook point called */
634} hookasync_data_t;
635
636/*
637 * 'destroy' callback of hook recursion ctx.
638 * The dynamically allocated context will be freed here, thereby proving
639 * this is actually called; otherwise tests would fail due to memory leak.
640 */
641static void
642destroy_hookactx(ns_hookasync_t **ctxp) {
643	ns_hookasync_t *ctx = *ctxp;
644
645	*ctxp = NULL;
646	isc_mem_putanddetach(&ctx->mctx, ctx, sizeof(*ctx));
647}
648
649/* 'cancel' callback of hook recursion ctx. */
650static void
651cancel_hookactx(ns_hookasync_t *ctx) {
652	/* Mark the hook data so the test can confirm this is called. */
653	((hookasync_data_t *)ctx->private)->canceled = true;
654}
655
656/* 'runasync' callback passed to ns_query_hookasync */
657static isc_result_t
658test_hookasync(query_ctx_t *qctx, isc_mem_t *memctx, void *arg,
659	       isc_task_t *task, isc_taskaction_t action, void *evarg,
660	       ns_hookasync_t **ctxp) {
661	hookasync_data_t *asdata = arg;
662	ns_hookasync_t *ctx = NULL;
663	ns_hook_resevent_t *rev = NULL;
664
665	if (asdata->start_result != ISC_R_SUCCESS) {
666		return (asdata->start_result);
667	}
668
669	ctx = isc_mem_get(memctx, sizeof(*ctx));
670	rev = (ns_hook_resevent_t *)isc_event_allocate(
671		memctx, task, NS_EVENT_HOOKASYNCDONE, action, evarg,
672		sizeof(*rev));
673
674	rev->hookpoint = asdata->hookpoint;
675	rev->origresult = DNS_R_NXDOMAIN;
676	rev->saved_qctx = qctx;
677	rev->ctx = ctx;
678	asdata->rev = rev;
679
680	*ctx = (ns_hookasync_t){ .private = asdata };
681	isc_mem_attach(memctx, &ctx->mctx);
682	ctx->destroy = destroy_hookactx;
683	ctx->cancel = cancel_hookactx;
684
685	*ctxp = ctx;
686	return (ISC_R_SUCCESS);
687}
688
689/*
690 * Main logic for hook actions.
691 * 'hookpoint' should identify the point that calls the hook.  It will be
692 * remembered in the hook data, so that the test can confirm which hook point
693 * was last used.
694 */
695static ns_hookresult_t
696hook_async_common(void *arg, void *data, isc_result_t *resultp,
697		  ns_hookpoint_t hookpoint) {
698	query_ctx_t *qctx = arg;
699	hookasync_data_t *asdata = data;
700	isc_result_t result;
701
702	asdata->qctx = *qctx; /* remember passed ctx for inspection */
703	asdata->lasthookpoint = hookpoint; /* ditto */
704
705	if (!asdata->async) {
706		/* Initial call to the hook; start recursion */
707		result = ns_query_hookasync(qctx, test_hookasync, asdata);
708		if (result == ISC_R_SUCCESS) {
709			asdata->async = true;
710		}
711	} else {
712		/*
713		 * Resume from the completion of async event.
714		 * fetchhandle should have been detached so that we can start
715		 * another async event or DNS recursive resolution.
716		 */
717		INSIST(qctx->client->fetchhandle == NULL);
718		asdata->async = false;
719		switch (hookpoint) {
720		case NS_QUERY_GOT_ANSWER_BEGIN:
721		case NS_QUERY_NODATA_BEGIN:
722		case NS_QUERY_NXDOMAIN_BEGIN:
723		case NS_QUERY_NCACHE_BEGIN:
724			INSIST(*resultp == DNS_R_NXDOMAIN);
725			break;
726		default:;
727		}
728	}
729
730	*resultp = ISC_R_UNSET;
731	return (NS_HOOK_RETURN);
732}
733
734static ns_hookresult_t
735hook_async_query_setup(void *arg, void *data, isc_result_t *resultp) {
736	return (hook_async_common(arg, data, resultp, NS_QUERY_SETUP));
737}
738
739static ns_hookresult_t
740hook_async_query_start_begin(void *arg, void *data, isc_result_t *resultp) {
741	return (hook_async_common(arg, data, resultp, NS_QUERY_START_BEGIN));
742}
743
744static ns_hookresult_t
745hook_async_query_lookup_begin(void *arg, void *data, isc_result_t *resultp) {
746	return (hook_async_common(arg, data, resultp, NS_QUERY_LOOKUP_BEGIN));
747}
748
749static ns_hookresult_t
750hook_async_query_resume_begin(void *arg, void *data, isc_result_t *resultp) {
751	return (hook_async_common(arg, data, resultp, NS_QUERY_RESUME_BEGIN));
752}
753
754static ns_hookresult_t
755hook_async_query_got_answer_begin(void *arg, void *data,
756				  isc_result_t *resultp) {
757	return (hook_async_common(arg, data, resultp,
758				  NS_QUERY_GOT_ANSWER_BEGIN));
759}
760
761static ns_hookresult_t
762hook_async_query_respond_any_begin(void *arg, void *data,
763				   isc_result_t *resultp) {
764	return (hook_async_common(arg, data, resultp,
765				  NS_QUERY_RESPOND_ANY_BEGIN));
766}
767
768static ns_hookresult_t
769hook_async_query_addanswer_begin(void *arg, void *data, isc_result_t *resultp) {
770	return (hook_async_common(arg, data, resultp,
771				  NS_QUERY_ADDANSWER_BEGIN));
772}
773
774static ns_hookresult_t
775hook_async_query_notfound_begin(void *arg, void *data, isc_result_t *resultp) {
776	return (hook_async_common(arg, data, resultp, NS_QUERY_NOTFOUND_BEGIN));
777}
778
779static ns_hookresult_t
780hook_async_query_prep_delegation_begin(void *arg, void *data,
781				       isc_result_t *resultp) {
782	return (hook_async_common(arg, data, resultp,
783				  NS_QUERY_PREP_DELEGATION_BEGIN));
784}
785
786static ns_hookresult_t
787hook_async_query_zone_delegation_begin(void *arg, void *data,
788				       isc_result_t *resultp) {
789	return (hook_async_common(arg, data, resultp,
790				  NS_QUERY_ZONE_DELEGATION_BEGIN));
791}
792
793static ns_hookresult_t
794hook_async_query_delegation_begin(void *arg, void *data,
795				  isc_result_t *resultp) {
796	return (hook_async_common(arg, data, resultp,
797				  NS_QUERY_DELEGATION_BEGIN));
798}
799
800static ns_hookresult_t
801hook_async_query_delegation_recurse_begin(void *arg, void *data,
802					  isc_result_t *resultp) {
803	return (hook_async_common(arg, data, resultp,
804				  NS_QUERY_DELEGATION_RECURSE_BEGIN));
805}
806
807static ns_hookresult_t
808hook_async_query_nodata_begin(void *arg, void *data, isc_result_t *resultp) {
809	return (hook_async_common(arg, data, resultp, NS_QUERY_NODATA_BEGIN));
810}
811
812static ns_hookresult_t
813hook_async_query_nxdomain_begin(void *arg, void *data, isc_result_t *resultp) {
814	return (hook_async_common(arg, data, resultp, NS_QUERY_NXDOMAIN_BEGIN));
815}
816
817static ns_hookresult_t
818hook_async_query_ncache_begin(void *arg, void *data, isc_result_t *resultp) {
819	return (hook_async_common(arg, data, resultp, NS_QUERY_NCACHE_BEGIN));
820}
821
822static ns_hookresult_t
823hook_async_query_cname_begin(void *arg, void *data, isc_result_t *resultp) {
824	return (hook_async_common(arg, data, resultp, NS_QUERY_CNAME_BEGIN));
825}
826
827static ns_hookresult_t
828hook_async_query_dname_begin(void *arg, void *data, isc_result_t *resultp) {
829	return (hook_async_common(arg, data, resultp, NS_QUERY_DNAME_BEGIN));
830}
831
832static ns_hookresult_t
833hook_async_query_respond_begin(void *arg, void *data, isc_result_t *resultp) {
834	return (hook_async_common(arg, data, resultp, NS_QUERY_RESPOND_BEGIN));
835}
836
837static ns_hookresult_t
838hook_async_query_response_begin(void *arg, void *data, isc_result_t *resultp) {
839	return (hook_async_common(arg, data, resultp,
840				  NS_QUERY_PREP_RESPONSE_BEGIN));
841}
842
843static ns_hookresult_t
844hook_async_query_done_begin(void *arg, void *data, isc_result_t *resultp) {
845	return (hook_async_common(arg, data, resultp, NS_QUERY_DONE_BEGIN));
846}
847
848/*
849 * hook on destroying actx.  Can't be used for async event, but we use this
850 * to remember the qctx at that point.
851 */
852static ns_hookresult_t
853ns_test_qctx_destroy_hook(void *arg, void *data, isc_result_t *resultp) {
854	query_ctx_t *qctx = arg;
855	hookasync_data_t *asdata = data;
856
857	asdata->qctx = *qctx; /* remember passed ctx for inspection */
858	*resultp = ISC_R_UNSET;
859	return (NS_HOOK_CONTINUE);
860}
861
862static void
863run_hookasync_test(const ns__query_hookasync_test_params_t *test) {
864	query_ctx_t *qctx = NULL;
865	isc_result_t result;
866	hookasync_data_t asdata = {
867		.async = false,
868		.canceled = false,
869		.start_result = test->start_result,
870		.hookpoint = test->hookpoint,
871	};
872	const ns_hook_t testhook = {
873		.action = test->action,
874		.action_data = &asdata,
875	};
876	const ns_hook_t destroyhook = {
877		.action = ns_test_qctx_destroy_hook,
878		.action_data = &asdata,
879	};
880	isc_quota_t *quota = NULL;
881	isc_statscounter_t srvfail_cnt;
882	bool expect_servfail = false;
883
884	/*
885	 * Prepare hooks.  We always begin with ns__query_start for simplicity.
886	 * Its action will specify various different resume points (unusual
887	 * in practice, but that's fine for the testing purpose).
888	 */
889	ns__hook_table = NULL;
890	ns_hooktable_create(mctx, &ns__hook_table);
891	ns_hook_add(ns__hook_table, mctx, NS_QUERY_START_BEGIN, &testhook);
892	if (test->hookpoint2 != NS_QUERY_START_BEGIN) {
893		/*
894		 * unless testing START_BEGIN itself, specify the hook for the
895		 * expected resume point, too.
896		 */
897		ns_hook_add(ns__hook_table, mctx, test->hookpoint2, &testhook);
898	}
899	ns_hook_add(ns__hook_table, mctx, NS_QUERY_QCTX_DESTROYED,
900		    &destroyhook);
901
902	{
903		const ns_test_qctx_create_params_t qctx_params = {
904			.qname = "test.example.com",
905			.qtype = dns_rdatatype_aaaa,
906		};
907		result = ns_test_qctx_create(&qctx_params, &qctx);
908		INSIST(result == ISC_R_SUCCESS);
909		qctx->client->sendcb = send_noop;
910	}
911
912	/*
913	 * Set recursion quota to the lowest possible value, then make it full
914	 * if we want to exercise a quota failure case.
915	 */
916	isc_quota_max(&sctx->recursionquota, 1);
917	if (!test->quota_ok) {
918		result = isc_quota_attach(&sctx->recursionquota, &quota);
919		INSIST(result == ISC_R_SUCCESS);
920	}
921
922	/* Remember SERVFAIL counter */
923	srvfail_cnt = ns_stats_get_counter(qctx->client->sctx->nsstats,
924					   ns_statscounter_servfail);
925
926	/*
927	 * If the query has been canceled, or async event didn't succeed,
928	 * SERVFAIL will have to be sent.  In this case we need to have
929	 * 'reqhandle' attach to the client's handle as it's detached in
930	 * query_error.
931	 */
932	if (test->start_result != ISC_R_SUCCESS || !test->quota_ok ||
933	    test->do_cancel)
934	{
935		expect_servfail = true;
936		isc_nmhandle_attach(qctx->client->handle,
937				    &qctx->client->reqhandle);
938	}
939
940	/*
941	 * Emulate query handling from query_start.
942	 * Specified hook should be called.
943	 */
944	qctx->client->state = NS_CLIENTSTATE_WORKING;
945	result = ns__query_start(qctx);
946	INSIST(result == ISC_R_UNSET);
947
948	/*
949	 * hook-triggered async event should be happening unless it hits
950	 * recursion quota limit or 'runasync' callback fails.
951	 */
952	INSIST(asdata.async ==
953	       (test->quota_ok && test->start_result == ISC_R_SUCCESS));
954
955	/*
956	 * Emulate cancel if so specified.
957	 * The cancel callback should be called.
958	 */
959	if (test->do_cancel) {
960		ns_query_cancel(qctx->client);
961	}
962	INSIST(asdata.canceled == test->do_cancel);
963
964	/* If async event has started, manually invoke the 'done' event. */
965	if (asdata.async) {
966		qctx->client->now = 0; /* set to sentinel before resume */
967		asdata.rev->ev_action(asdata.rev->ev_sender,
968				      (isc_event_t *)asdata.rev);
969
970		/* Confirm necessary cleanup has been performed. */
971		INSIST(qctx->client->query.hookactx == NULL);
972		INSIST(qctx->client->state == NS_CLIENTSTATE_WORKING);
973		INSIST(qctx->client->recursionquota == NULL);
974		INSIST(ns_stats_get_counter(qctx->client->sctx->nsstats,
975					    ns_statscounter_recursclients) ==
976		       0);
977		INSIST(!ISC_LINK_LINKED(qctx->client, rlink));
978		if (!test->do_cancel) {
979			/*
980			 * In the normal case the client's timestamp is updated
981			 * and the query handling has been resumed from the
982			 * expected point.
983			 */
984			INSIST(qctx->client->now != 0);
985			INSIST(asdata.lasthookpoint == test->hookpoint2);
986		}
987	} else {
988		INSIST(qctx->client->query.hookactx == NULL);
989	}
990
991	/*
992	 * Confirm SERVFAIL has been sent if it was expected.
993	 * Also, the last-generated qctx should have detach_client being true.
994	 */
995	if (expect_servfail) {
996		INSIST(ns_stats_get_counter(qctx->client->sctx->nsstats,
997					    ns_statscounter_servfail) ==
998		       srvfail_cnt + 1);
999		if (test->do_cancel) {
1000			/* qctx was created on resume and copied in hook */
1001			INSIST(asdata.qctx.detach_client);
1002		} else {
1003			INSIST(qctx->detach_client);
1004		}
1005	}
1006
1007	/*
1008	 * Cleanup.  Note that we've kept 'qctx' until now; otherwise
1009	 * qctx->client may have been invalidated while we still need it.
1010	 */
1011	ns_test_qctx_destroy(&qctx);
1012	ns_hooktable_free(mctx, (void **)&ns__hook_table);
1013	if (quota != NULL) {
1014		isc_quota_detach(&quota);
1015	}
1016}
1017
1018ISC_RUN_TEST_IMPL(ns_query_hookasync) {
1019	size_t i;
1020
1021	UNUSED(state);
1022
1023	const ns__query_hookasync_test_params_t tests[] = {
1024		{
1025			NS_TEST_ID("normal case"),
1026			NS_QUERY_START_BEGIN,
1027			NS_QUERY_START_BEGIN,
1028			hook_async_query_start_begin,
1029			ISC_R_SUCCESS,
1030			true,
1031			false,
1032		},
1033		{
1034			NS_TEST_ID("quota fail"),
1035			NS_QUERY_START_BEGIN,
1036			NS_QUERY_START_BEGIN,
1037			hook_async_query_start_begin,
1038			ISC_R_SUCCESS,
1039			false,
1040			false,
1041		},
1042		{
1043			NS_TEST_ID("start fail"),
1044			NS_QUERY_START_BEGIN,
1045			NS_QUERY_START_BEGIN,
1046			hook_async_query_start_begin,
1047			ISC_R_FAILURE,
1048			true,
1049			false,
1050		},
1051		{
1052			NS_TEST_ID("query cancel"),
1053			NS_QUERY_START_BEGIN,
1054			NS_QUERY_START_BEGIN,
1055			hook_async_query_start_begin,
1056			ISC_R_SUCCESS,
1057			true,
1058			true,
1059		},
1060		/*
1061		 * The rest of the test case just confirms supported hookpoints
1062		 * with the same test logic.
1063		 */
1064		{
1065			NS_TEST_ID("async from setup"),
1066			NS_QUERY_SETUP,
1067			NS_QUERY_SETUP,
1068			hook_async_query_setup,
1069			ISC_R_SUCCESS,
1070			true,
1071			false,
1072		},
1073		{
1074			NS_TEST_ID("async from lookup"),
1075			NS_QUERY_LOOKUP_BEGIN,
1076			NS_QUERY_LOOKUP_BEGIN,
1077			hook_async_query_lookup_begin,
1078			ISC_R_SUCCESS,
1079			true,
1080			false,
1081		},
1082		{
1083			NS_TEST_ID("async from resume"),
1084			NS_QUERY_RESUME_BEGIN,
1085			NS_QUERY_RESUME_BEGIN,
1086			hook_async_query_resume_begin,
1087			ISC_R_SUCCESS,
1088			true,
1089			false,
1090		},
1091		{
1092			NS_TEST_ID("async from resume restored"),
1093			NS_QUERY_RESUME_RESTORED,
1094			NS_QUERY_RESUME_BEGIN,
1095			hook_async_query_resume_begin,
1096			ISC_R_SUCCESS,
1097			true,
1098			false,
1099		},
1100		{
1101			NS_TEST_ID("async from gotanswer"),
1102			NS_QUERY_GOT_ANSWER_BEGIN,
1103			NS_QUERY_GOT_ANSWER_BEGIN,
1104			hook_async_query_got_answer_begin,
1105			ISC_R_SUCCESS,
1106			true,
1107			false,
1108		},
1109		{
1110			NS_TEST_ID("async from respond any"),
1111			NS_QUERY_RESPOND_ANY_BEGIN,
1112			NS_QUERY_RESPOND_ANY_BEGIN,
1113			hook_async_query_respond_any_begin,
1114			ISC_R_SUCCESS,
1115			true,
1116			false,
1117		},
1118		{
1119			NS_TEST_ID("async from add answer"),
1120			NS_QUERY_ADDANSWER_BEGIN,
1121			NS_QUERY_ADDANSWER_BEGIN,
1122			hook_async_query_addanswer_begin,
1123			ISC_R_SUCCESS,
1124			true,
1125			false,
1126		},
1127		{
1128			NS_TEST_ID("async from notfound"),
1129			NS_QUERY_NOTFOUND_BEGIN,
1130			NS_QUERY_NOTFOUND_BEGIN,
1131			hook_async_query_notfound_begin,
1132			ISC_R_SUCCESS,
1133			true,
1134			false,
1135		},
1136		{
1137			NS_TEST_ID("async from prep delegation"),
1138			NS_QUERY_PREP_DELEGATION_BEGIN,
1139			NS_QUERY_PREP_DELEGATION_BEGIN,
1140			hook_async_query_prep_delegation_begin,
1141			ISC_R_SUCCESS,
1142			true,
1143			false,
1144		},
1145		{
1146			NS_TEST_ID("async from zone delegation"),
1147			NS_QUERY_ZONE_DELEGATION_BEGIN,
1148			NS_QUERY_ZONE_DELEGATION_BEGIN,
1149			hook_async_query_zone_delegation_begin,
1150			ISC_R_SUCCESS,
1151			true,
1152			false,
1153		},
1154		{
1155			NS_TEST_ID("async from delegation"),
1156			NS_QUERY_DELEGATION_BEGIN,
1157			NS_QUERY_DELEGATION_BEGIN,
1158			hook_async_query_delegation_begin,
1159			ISC_R_SUCCESS,
1160			true,
1161			false,
1162		},
1163		{
1164			NS_TEST_ID("async from async delegation"),
1165			NS_QUERY_DELEGATION_RECURSE_BEGIN,
1166			NS_QUERY_DELEGATION_RECURSE_BEGIN,
1167			hook_async_query_delegation_recurse_begin,
1168			ISC_R_SUCCESS,
1169			true,
1170			false,
1171		},
1172		{
1173			NS_TEST_ID("async from nodata"),
1174			NS_QUERY_NODATA_BEGIN,
1175			NS_QUERY_NODATA_BEGIN,
1176			hook_async_query_nodata_begin,
1177			ISC_R_SUCCESS,
1178			true,
1179			false,
1180		},
1181		{
1182			NS_TEST_ID("async from nxdomain"),
1183			NS_QUERY_NXDOMAIN_BEGIN,
1184			NS_QUERY_NXDOMAIN_BEGIN,
1185			hook_async_query_nxdomain_begin,
1186			ISC_R_SUCCESS,
1187			true,
1188			false,
1189		},
1190		{
1191			NS_TEST_ID("async from ncache"),
1192			NS_QUERY_NCACHE_BEGIN,
1193			NS_QUERY_NCACHE_BEGIN,
1194			hook_async_query_ncache_begin,
1195			ISC_R_SUCCESS,
1196			true,
1197			false,
1198		},
1199		{
1200			NS_TEST_ID("async from CNAME"),
1201			NS_QUERY_CNAME_BEGIN,
1202			NS_QUERY_CNAME_BEGIN,
1203			hook_async_query_cname_begin,
1204			ISC_R_SUCCESS,
1205			true,
1206			false,
1207		},
1208		{
1209			NS_TEST_ID("async from DNAME"),
1210			NS_QUERY_DNAME_BEGIN,
1211			NS_QUERY_DNAME_BEGIN,
1212			hook_async_query_dname_begin,
1213			ISC_R_SUCCESS,
1214			true,
1215			false,
1216		},
1217		{
1218			NS_TEST_ID("async from prep response"),
1219			NS_QUERY_PREP_RESPONSE_BEGIN,
1220			NS_QUERY_PREP_RESPONSE_BEGIN,
1221			hook_async_query_response_begin,
1222			ISC_R_SUCCESS,
1223			true,
1224			false,
1225		},
1226		{
1227			NS_TEST_ID("async from respond"),
1228			NS_QUERY_RESPOND_BEGIN,
1229			NS_QUERY_RESPOND_BEGIN,
1230			hook_async_query_respond_begin,
1231			ISC_R_SUCCESS,
1232			true,
1233			false,
1234		},
1235		{
1236			NS_TEST_ID("async from done begin"),
1237			NS_QUERY_DONE_BEGIN,
1238			NS_QUERY_DONE_BEGIN,
1239			hook_async_query_done_begin,
1240			ISC_R_SUCCESS,
1241			true,
1242			false,
1243		},
1244		{
1245			NS_TEST_ID("async from done send"),
1246			NS_QUERY_DONE_SEND,
1247			NS_QUERY_DONE_BEGIN,
1248			hook_async_query_done_begin,
1249			ISC_R_SUCCESS,
1250			true,
1251			false,
1252		},
1253	};
1254
1255	for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
1256		run_hookasync_test(&tests[i]);
1257	}
1258}
1259
1260/*****
1261***** tests for higher level ("e2e") behavior of ns_query_hookasync().
1262***** It exercises overall behavior for some selected cases, while
1263***** ns__query_hookasync_test exercises implementation details for a
1264***** simple scenario and for all supported hook points.
1265*****/
1266
1267/*%
1268 * Structure containing parameters for ns__query_hookasync_e2e_test().
1269 */
1270typedef struct {
1271	const ns_test_id_t id;	   /* libns test identifier */
1272	const char *qname;	   /* QNAME */
1273	ns_hookpoint_t hookpoint;  /* hook point specified for resume */
1274	isc_result_t start_result; /* result of 'runasync' */
1275	bool do_cancel;		   /* true if query should be canceled
1276				    * in test */
1277	dns_rcode_t expected_rcode;
1278} ns__query_hookasync_e2e_test_params_t;
1279
1280/* data structure passed from tests to hooks */
1281typedef struct hookasync_e2e_data {
1282	bool async;		   /* true if in a hook-triggered
1283				    * asynchronous process */
1284	ns_hook_resevent_t *rev;   /* resume event sent on completion */
1285	ns_hookpoint_t hookpoint;  /* specifies where to resume */
1286	isc_result_t start_result; /* result of 'runasync' */
1287	dns_rcode_t expected_rcode;
1288	bool done; /* if SEND_DONE hook is called */
1289} hookasync_e2e_data_t;
1290
1291/* Cancel callback.  Just need to be defined, it doesn't have to do anything. */
1292static void
1293cancel_e2ehookactx(ns_hookasync_t *ctx) {
1294	UNUSED(ctx);
1295}
1296
1297/* 'runasync' callback passed to ns_query_hookasync */
1298static isc_result_t
1299test_hookasync_e2e(query_ctx_t *qctx, isc_mem_t *memctx, void *arg,
1300		   isc_task_t *task, isc_taskaction_t action, void *evarg,
1301		   ns_hookasync_t **ctxp) {
1302	ns_hookasync_t *ctx = NULL;
1303	ns_hook_resevent_t *rev = NULL;
1304	hookasync_e2e_data_t *asdata = arg;
1305
1306	if (asdata->start_result != ISC_R_SUCCESS) {
1307		return (asdata->start_result);
1308	}
1309
1310	ctx = isc_mem_get(memctx, sizeof(*ctx));
1311	rev = (ns_hook_resevent_t *)isc_event_allocate(
1312		memctx, task, NS_EVENT_HOOKASYNCDONE, action, evarg,
1313		sizeof(*rev));
1314
1315	rev->hookpoint = asdata->hookpoint;
1316	rev->saved_qctx = qctx;
1317	rev->ctx = ctx;
1318	asdata->rev = rev;
1319
1320	*ctx = (ns_hookasync_t){ .private = asdata };
1321	isc_mem_attach(memctx, &ctx->mctx);
1322	ctx->destroy = destroy_hookactx;
1323	ctx->cancel = cancel_e2ehookactx;
1324
1325	*ctxp = ctx;
1326	return (ISC_R_SUCCESS);
1327}
1328
1329static ns_hookresult_t
1330hook_async_e2e(void *arg, void *data, isc_result_t *resultp) {
1331	query_ctx_t *qctx = arg;
1332	hookasync_e2e_data_t *asdata = data;
1333	isc_result_t result;
1334
1335	if (!asdata->async) {
1336		/* Initial call to the hook; start async event */
1337		result = ns_query_hookasync(qctx, test_hookasync_e2e, asdata);
1338		if (result != ISC_R_SUCCESS) {
1339			*resultp = result;
1340			return (NS_HOOK_RETURN);
1341		}
1342
1343		asdata->async = true;
1344		asdata->rev->origresult = *resultp; /* save it for resume */
1345		*resultp = ISC_R_UNSET;
1346		return (NS_HOOK_RETURN);
1347	} else {
1348		/* Resume from the completion of async event */
1349		asdata->async = false;
1350		/* Don't touch 'resultp' */
1351		return (NS_HOOK_CONTINUE);
1352	}
1353}
1354
1355/*
1356 * Check whether the final response has expected the RCODE according to
1357 * the test scenario.
1358 */
1359static ns_hookresult_t
1360hook_donesend(void *arg, void *data, isc_result_t *resultp) {
1361	query_ctx_t *qctx = arg;
1362	hookasync_e2e_data_t *asdata = data;
1363
1364	INSIST(qctx->client->message->rcode == asdata->expected_rcode);
1365	asdata->done = true; /* Let the test know this hook is called */
1366	*resultp = ISC_R_UNSET;
1367	return (NS_HOOK_CONTINUE);
1368}
1369
1370static void
1371run_hookasync_e2e_test(const ns__query_hookasync_e2e_test_params_t *test) {
1372	query_ctx_t *qctx = NULL;
1373	isc_result_t result;
1374	hookasync_e2e_data_t asdata = {
1375		.async = false,
1376		.hookpoint = test->hookpoint,
1377		.start_result = test->start_result,
1378		.expected_rcode = test->expected_rcode,
1379		.done = false,
1380	};
1381	const ns_hook_t donesend_hook = {
1382		.action = hook_donesend,
1383		.action_data = &asdata,
1384	};
1385	const ns_hook_t hook = {
1386		.action = hook_async_e2e,
1387		.action_data = &asdata,
1388	};
1389	const ns_test_qctx_create_params_t qctx_params = {
1390		.qname = test->qname,
1391		.qtype = dns_rdatatype_a,
1392		.with_cache = true,
1393	};
1394
1395	ns__hook_table = NULL;
1396	ns_hooktable_create(mctx, &ns__hook_table);
1397	ns_hook_add(ns__hook_table, mctx, test->hookpoint, &hook);
1398	ns_hook_add(ns__hook_table, mctx, NS_QUERY_DONE_SEND, &donesend_hook);
1399
1400	result = ns_test_qctx_create(&qctx_params, &qctx);
1401	INSIST(result == ISC_R_SUCCESS);
1402
1403	isc_sockaddr_any(&qctx->client->peeraddr); /* for sortlist */
1404	qctx->client->sendcb = send_noop;
1405
1406	/* Load a zone.  it should have ns.foo/A */
1407	result = ns_test_serve_zone("foo", TESTS_DIR "/testdata/query/foo.db",
1408				    qctx->client->view);
1409	INSIST(result == ISC_R_SUCCESS);
1410
1411	/*
1412	 * We expect to have a response sent all cases, so we need to
1413	 * setup reqhandle (which will be detached on the send).
1414	 */
1415	isc_nmhandle_attach(qctx->client->handle, &qctx->client->reqhandle);
1416
1417	/* Handle the query.  hook-based async event will be triggered. */
1418	qctx->client->state = NS_CLIENTSTATE_WORKING;
1419	ns__query_start(qctx);
1420
1421	/* If specified cancel the query at this point. */
1422	if (test->do_cancel) {
1423		ns_query_cancel(qctx->client);
1424	}
1425
1426	if (test->start_result == ISC_R_SUCCESS) {
1427		/*
1428		 * If async event has started, manually invoke the done event.
1429		 */
1430		INSIST(asdata.async);
1431		asdata.rev->ev_action(asdata.rev->ev_sender,
1432				      (isc_event_t *)asdata.rev);
1433
1434		/*
1435		 * Usually 'async' is reset to false on the 2nd call to
1436		 * the hook.  But the hook isn't called if the query is
1437		 * canceled.
1438		 */
1439		INSIST(asdata.done == !test->do_cancel);
1440		INSIST(asdata.async == test->do_cancel);
1441	} else {
1442		INSIST(!asdata.async);
1443	}
1444
1445	/* Cleanup */
1446	ns_test_qctx_destroy(&qctx);
1447	ns_test_cleanup_zone();
1448	ns_hooktable_free(mctx, (void **)&ns__hook_table);
1449}
1450
1451ISC_RUN_TEST_IMPL(ns_query_hookasync_e2e) {
1452	UNUSED(state);
1453
1454	const ns__query_hookasync_e2e_test_params_t tests[] = {
1455		{
1456			NS_TEST_ID("positive answer"),
1457			"ns.foo",
1458			NS_QUERY_GOT_ANSWER_BEGIN,
1459			ISC_R_SUCCESS,
1460			false,
1461			dns_rcode_noerror,
1462		},
1463		{
1464			NS_TEST_ID("NXDOMAIN"),
1465			"notexist.foo",
1466			NS_QUERY_NXDOMAIN_BEGIN,
1467			ISC_R_SUCCESS,
1468			false,
1469			dns_rcode_nxdomain,
1470		},
1471		{
1472			NS_TEST_ID("async fail"),
1473			"ns.foo",
1474			NS_QUERY_DONE_BEGIN,
1475			ISC_R_FAILURE,
1476			false,
1477			-1,
1478		},
1479		{
1480			NS_TEST_ID("cancel query"),
1481			"ns.foo",
1482			NS_QUERY_DONE_BEGIN,
1483			ISC_R_SUCCESS,
1484			true,
1485			-1,
1486		},
1487	};
1488
1489	for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
1490		run_hookasync_e2e_test(&tests[i]);
1491	}
1492}
1493
1494ISC_TEST_LIST_START
1495
1496ISC_TEST_ENTRY_CUSTOM(ns_query_sfcache, setup_test, teardown_test)
1497ISC_TEST_ENTRY_CUSTOM(ns_query_start, setup_test, teardown_test)
1498ISC_TEST_ENTRY_CUSTOM(ns_query_hookasync, setup_test, teardown_test)
1499ISC_TEST_ENTRY_CUSTOM(ns_query_hookasync_e2e, setup_test, teardown_test)
1500
1501ISC_TEST_LIST_END
1502ISC_TEST_MAIN
1503