From 01962a393250125bb86c638b1786fbcc18a246cc Mon Sep 17 00:00:00 2001 From: Matt Jordan Date: Wed, 30 Dec 2015 10:52:28 -0600 Subject: [PATCH] tests/test_http_media_cache: Add unit tests for res_http_media_cache This patch adds unit tests for res_http_media cache, that covers nominal creation and retrieval - and through them as well, staleness and deletion checks. In addition, this patch adds tests that covers the interaction of various HTTP headers, including Expires, Etag, and Cache-Control. ASTERISK-25654 Change-Id: I2db101e307c863857fe416d6f5bf4cace9ac7cf5 --- tests/test_http_media_cache.c | 702 ++++++++++++++++++++++++++++++++++ 1 file changed, 702 insertions(+) create mode 100644 tests/test_http_media_cache.c diff --git a/tests/test_http_media_cache.c b/tests/test_http_media_cache.c new file mode 100644 index 0000000000..6a57433cbe --- /dev/null +++ b/tests/test_http_media_cache.c @@ -0,0 +1,702 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Matt Jordan + * + * Matt Jordan + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! + * \file + * \brief Tests for the HTTP media cache backend + * + * \author \verbatim Matt Jordan \endverbatim + * + * \ingroup tests + */ + +/*** MODULEINFO + TEST_FRAMEWORK + curl + res_http_media_cache + core + ***/ + +#include "asterisk.h" + +ASTERISK_REGISTER_FILE() + +#include + +#include "asterisk/module.h" +#include "asterisk/http.h" +#include "asterisk/bucket.h" +#include "asterisk/test.h" + +#define CATEGORY "/res/http_media_cache/" + +#define TEST_URI "test_media_cache" + +struct test_options { + int status_code; + int send_file; + struct { + int s_maxage; + int maxage; + int no_cache; + int must_revalidate; + } cache_control; + struct timeval expires; + const char *status_text; + const char *etag; +}; + +static struct test_options options; + +static char server_uri[512]; + +#define VALIDATE_EXPIRES(test, bucket_file, expected, delta) do { \ + RAII_VAR(struct ast_bucket_metadata *, metadata, ast_bucket_file_metadata_get((bucket_file), "__actual_expires"), ao2_cleanup); \ + int actual_expires; \ + ast_test_validate(test, metadata != NULL); \ + ast_test_validate(test, sscanf(metadata->value, "%d", &actual_expires) == 1); \ + ast_test_validate(test, (((expected) + (delta) > actual_expires) && ((expected) - (delta) < actual_expires))); \ +} while (0) + +#define VALIDATE_STR_METADATA(test, bucket_file, key, expected) do { \ + RAII_VAR(struct ast_bucket_metadata *, metadata, ast_bucket_file_metadata_get((bucket_file), (key)), ao2_cleanup); \ + ast_test_validate(test, metadata != NULL); \ + ast_test_validate(test, !strcmp(metadata->value, (expected))); \ +} while (0) + +#define SET_OR_APPEND_CACHE_CONTROL(str) do { \ + if (!ast_str_strlen((str))) { \ + ast_str_set(&(str), 0, "%s", "cache-control: "); \ + } else { \ + ast_str_append(&(str), 0, "%s", ", "); \ + } \ +} while (0) + +static int http_callback(struct ast_tcptls_session_instance *ser, const struct ast_http_uri *urih, const char *uri, enum ast_http_method method, struct ast_variable *get_params, struct ast_variable *headers) +{ + char file_name[64] = "/tmp/test-media-cache-XXXXXX"; + struct ast_str *http_header = ast_str_create(128); + struct ast_str *cache_control = ast_str_create(128); + int fd = -1; + int unmodified = 0; + int send_file = options.send_file && method == AST_HTTP_GET; + + if (!http_header) { + goto error; + } + + if (send_file) { + char buf[1024]; + + fd = mkstemp(file_name); + if (fd == -1) { + ast_log(LOG_ERROR, "Unable to open temp file for testing: %s (%d)", strerror(errno), errno); + goto error; + } + + memset(buf, 1, sizeof(buf)); + if (write(fd, buf, sizeof(buf)) != sizeof(buf)) { + ast_log(LOG_ERROR, "Failed to write expected number of bytes to pipe\n"); + close(fd); + goto error; + } + close(fd); + + fd = open(file_name, 0); + if (fd == -1) { + ast_log(LOG_ERROR, "Unable to open temp file for testing: %s (%d)", strerror(errno), errno); + goto error; + } + } + + if (options.cache_control.maxage) { + SET_OR_APPEND_CACHE_CONTROL(cache_control); + ast_str_append(&cache_control, 0, "max-age=%d", options.cache_control.maxage); + } + + if (options.cache_control.s_maxage) { + SET_OR_APPEND_CACHE_CONTROL(cache_control); + ast_str_append(&cache_control, 0, "s-maxage=%d", options.cache_control.s_maxage); + } + + if (options.cache_control.no_cache) { + SET_OR_APPEND_CACHE_CONTROL(cache_control); + ast_str_append(&cache_control, 0, "%s", "no-cache"); + } + + if (options.cache_control.must_revalidate) { + SET_OR_APPEND_CACHE_CONTROL(cache_control); + ast_str_append(&cache_control, 0, "%s", "must-revalidate"); + } + + if (ast_str_strlen(cache_control)) { + ast_str_append(&http_header, 0, "%s\r\n", ast_str_buffer(cache_control)); + } + + if (options.expires.tv_sec) { + struct ast_tm now_time; + char tmbuf[64]; + + ast_localtime(&options.expires, &now_time, NULL); + ast_strftime(tmbuf, sizeof(tmbuf), "%a, %d %b %Y %T %z", &now_time); + ast_str_append(&http_header, 0, "Expires: %s\r\n", tmbuf); + } + + if (!ast_strlen_zero(options.etag)) { + struct ast_variable *v; + + ast_str_append(&http_header, 0, "ETag: %s\r\n", options.etag); + for (v = headers; v; v = v->next) { + if (!strcasecmp(v->name, "If-None-Match") && !strcasecmp(v->value, options.etag)) { + unmodified = 1; + break; + } + } + } + + if (!unmodified) { + ast_http_send(ser, method, options.status_code, options.status_text, http_header, NULL, send_file ? fd : 0, 1); + } else { + ast_http_send(ser, method, 304, "Not Modified", http_header, NULL, 0, 1); + if (send_file) { + close(fd); + } + } + + if (send_file) { + unlink(file_name); + } + + ast_free(cache_control); + + return 0; + +error: + ast_free(http_header); + ast_free(cache_control); + ast_http_request_close_on_completion(ser); + ast_http_error(ser, 418, "I'm a Teapot", "Please don't ask me to brew coffee."); + + return 0; +} + +static struct ast_http_uri test_uri = { + .description = "HTTP Media Cache Test URI", + .uri = TEST_URI, + .callback = http_callback, + .has_subtree = 1, + .data = NULL, + .key = __FILE__, +}; + +static int pre_test_cb(struct ast_test_info *info, struct ast_test *test) +{ + memset(&options, 0, sizeof(options)); + + return 0; +} + +static void bucket_file_cleanup(void *obj) +{ + struct ast_bucket_file *bucket_file = obj; + + if (bucket_file) { + ast_bucket_file_delete(bucket_file); + ao2_ref(bucket_file, -1); + } +} + +AST_TEST_DEFINE(retrieve_cache_control_directives) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval of a resource with Cache-Control directives that affect staleness"; + info->description = + "This test covers retrieval of a resource with the Cache-Control header,\n" + "which specifies no-cache and/or must-revalidate."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + + ast_test_status_update(test, "Testing no-cache...\n"); + options.cache_control.no_cache = 1; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing no-cache with ETag...\n"); + options.cache_control.no_cache = 1; + options.etag = "123456789"; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + options.etag = NULL; + + ast_test_status_update(test, "Testing no-cache with max-age...\n"); + options.cache_control.no_cache = 1; + options.cache_control.maxage = 300; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + bucket_file_cleanup(bucket_file); + + options.cache_control.maxage = 0; + options.cache_control.no_cache = 0; + + ast_test_status_update(test, "Testing must-revalidate...\n"); + options.cache_control.must_revalidate = 1; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing must-revalidate with ETag...\n"); + options.cache_control.must_revalidate = 1; + options.etag = "123456789"; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + options.etag = NULL; + + ast_test_status_update(test, "Testing must-revalidate with max-age...\n"); + options.cache_control.must_revalidate = 1; + options.cache_control.maxage = 300; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_cache_control_age) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval of a resource with age specifiers in Cache-Control"; + info->description = + "This test covers retrieval of a resource with the Cache-Control header,\n" + "which specifies max-age and/or s-maxage. The test verifies proper precedence\n" + "ordering of the header attributes, along with its relation if the Expires\n" + "header is present."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + + ast_test_status_update(test, "Testing max-age...\n"); + options.cache_control.maxage = 300; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing s-maxage...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 0; + options.cache_control.s_maxage = 300; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing max-age and s-maxage...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 300; + options.cache_control.s_maxage = 600; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 600, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing max-age and Expires...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 300; + options.cache_control.s_maxage = 0; + options.expires.tv_sec = now.tv_sec + 3000; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing s-maxage and Expires...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 0; + options.cache_control.s_maxage = 300; + options.expires.tv_sec = now.tv_sec + 3000; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing s-maxage and Expires...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 0; + options.cache_control.s_maxage = 300; + options.expires.tv_sec = now.tv_sec + 3000; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 300, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + bucket_file_cleanup(bucket_file); + + ast_test_status_update(test, "Testing max-age, s-maxage, and Expires...\n"); + now = ast_tvnow(); + options.cache_control.maxage = 300; + options.cache_control.s_maxage = 600; + options.expires.tv_sec = now.tv_sec + 3000; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 600, 1); + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_etag_expired) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval of an expired resource with an ETag"; + info->description = + "This test covers a staleness check of a resource with an ETag\n" + "that has also expired. It guarantees that even if a resource\n" + "is expired, we will still not consider it stale if the resource\n" + "has not changed per the ETag value."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + options.etag = "123456789"; + options.expires.tv_sec = now.tv_sec - 1; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, !strcmp(uri, ast_sorcery_object_get_id(bucket_file))); + ast_test_validate(test, !ast_strlen_zero(bucket_file->path)); + VALIDATE_STR_METADATA(test, bucket_file, "etag", options.etag); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec - 1, 1); + + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_expires) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval with explicit expiration"; + info->description = + "This test covers retrieving a resource that has an Expires.\n" + "After retrieval of the resource, staleness is checked. With\n" + "a non-expired resource, we expect the resource to not be stale.\n" + "When the expiration has occurred, we expect the staleness check\n" + "to fail."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + options.expires.tv_sec = now.tv_sec + 3000; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, !strcmp(uri, ast_sorcery_object_get_id(bucket_file))); + ast_test_validate(test, !ast_strlen_zero(bucket_file->path)); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec + 3000, 1); + + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + + /* Clean up previous result */ + bucket_file_cleanup(bucket_file); + + options.expires.tv_sec = now.tv_sec - 1; + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec - 1, 1); + + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_etag) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test retrieval with an ETag"; + info->description = + "This test covers retrieving a resource that has an ETag.\n" + "After retrieval of the resource, staleness is checked. With\n" + "matching ETags, we expect the resource to not be stale. When\n" + "the ETag does not match, we expect the resource to be stale."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + options.etag = "123456789"; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, !strcmp(uri, ast_sorcery_object_get_id(bucket_file))); + ast_test_validate(test, !ast_strlen_zero(bucket_file->path)); + VALIDATE_STR_METADATA(test, bucket_file, "etag", options.etag); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec, 1); + + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 0); + + options.etag = "99999999"; + ast_test_validate(test, ast_bucket_file_is_stale(bucket_file) == 1); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(retrieve_nominal) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test nominal retrieval"; + info->description = + "Test nominal retrieval of a resource."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_retrieve(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, !strcmp(uri, ast_sorcery_object_get_id(bucket_file))); + ast_test_validate(test, !ast_strlen_zero(bucket_file->path)); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec, 1); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(create_nominal) +{ + RAII_VAR(struct ast_bucket_file *, bucket_file, NULL, bucket_file_cleanup); + struct timeval now = ast_tvnow(); + char uri[1024]; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Test nominal creation"; + info->description = + "Test nominal creation of a resource."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + options.send_file = 1; + options.status_code = 200; + options.status_text = "OK"; + + snprintf(uri, sizeof(uri), "%s/%s", server_uri, "foo.wav"); + + bucket_file = ast_bucket_file_alloc(uri); + ast_test_validate(test, bucket_file != NULL); + ast_test_validate(test, ast_bucket_file_temporary_create(bucket_file) == 0); + ast_test_validate(test, ast_bucket_file_create(bucket_file) == 0); + VALIDATE_EXPIRES(test, bucket_file, now.tv_sec, 1); + + return AST_TEST_PASS; +} + + +static int process_config(int reload) +{ + struct ast_config *config; + struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + const char *bindaddr; + const char *bindport; + const char *prefix; + const char *enabled; + + config = ast_config_load("http.conf", config_flags); + if (!config || config == CONFIG_STATUS_FILEINVALID) { + return -1; + } else if (config == CONFIG_STATUS_FILEUNCHANGED) { + return 0; + } + + enabled = ast_config_option(config, "general", "enabled"); + if (!enabled || ast_false(enabled)) { + ast_config_destroy(config); + return -1; + } + + /* Construct our Server URI */ + bindaddr = ast_config_option(config, "general", "bindaddr"); + if (!bindaddr) { + ast_config_destroy(config); + return -1; + } + + bindport = ast_config_option(config, "general", "bindport"); + if (!bindport) { + bindport = "8088"; + } + + prefix = ast_config_option(config, "general", "prefix"); + + snprintf(server_uri, sizeof(server_uri), "http://%s:%s%s/%s", bindaddr, bindport, S_OR(prefix, ""), TEST_URI); + + ast_config_destroy(config); + + return 0; +} + +static int reload_module(void) +{ + return process_config(1); +} + +static int load_module(void) +{ + if (process_config(0)) { + return AST_MODULE_LOAD_DECLINE; + } + + if (ast_http_uri_link(&test_uri)) { + return AST_MODULE_LOAD_DECLINE; + } + + AST_TEST_REGISTER(create_nominal); + + AST_TEST_REGISTER(retrieve_nominal); + AST_TEST_REGISTER(retrieve_etag); + AST_TEST_REGISTER(retrieve_expires); + AST_TEST_REGISTER(retrieve_etag_expired); + AST_TEST_REGISTER(retrieve_cache_control_age); + AST_TEST_REGISTER(retrieve_cache_control_directives); + + ast_test_register_init(CATEGORY, pre_test_cb); + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_http_uri_unlink(&test_uri); + + AST_TEST_UNREGISTER(create_nominal); + + AST_TEST_UNREGISTER(retrieve_nominal); + AST_TEST_UNREGISTER(retrieve_etag); + AST_TEST_UNREGISTER(retrieve_expires); + AST_TEST_UNREGISTER(retrieve_etag_expired); + AST_TEST_UNREGISTER(retrieve_cache_control_age); + AST_TEST_UNREGISTER(retrieve_cache_control_directives); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "HTTP Media Cache Backend Tests", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .reload = reload_module, + .unload = unload_module, + .load_pri = AST_MODPRI_DEFAULT, + );