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