diff --git a/src/common/src/common-macros-private.h b/src/common/src/common-macros-private.h index d00534c347d..068b09470fa 100644 --- a/src/common/src/common-macros-private.h +++ b/src/common/src/common-macros-private.h @@ -96,4 +96,16 @@ #define MC_DISABLE_IMPLICIT_WARNING_END #endif +// Disable the -Wcast-qual warning +#if defined(__GNUC__) +#define MC_DISABLE_CAST_QUAL_WARNING_BEGIN MC_PRAGMA_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic ignored \"-Wcast-qual\"") +#define MC_DISABLE_CAST_QUAL_WARNING_END MC_PRAGMA_DIAGNOSTIC_POP +#elif defined(__clang__) +#define MC_DISABLE_CAST_QUAL_WARNING_BEGIN MC_PRAGMA_DIAGNOSTIC_PUSH _Pragma("clang diagnostic ignored \"-Wcast-qual\"") +#define MC_DISABLE_CAST_QUAL_WARNING_END MC_PRAGMA_DIAGNOSTIC_POP +#else +#define MC_DISABLE_CAST_QUAL_WARNING_BEGIN +#define MC_DISABLE_CAST_QUAL_WARNING_END +#endif + #endif /* MONGO_C_DRIVER_COMMON_MACROS_PRIVATE_H */ diff --git a/src/libmongoc/CMakeLists.txt b/src/libmongoc/CMakeLists.txt index 6243b4b47db..e87580622a4 100644 --- a/src/libmongoc/CMakeLists.txt +++ b/src/libmongoc/CMakeLists.txt @@ -594,6 +594,7 @@ set (MONGOC_SOURCES ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-log-and-monitor-private.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-memcmp.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-cmd.c + ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-oidc-cache.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-oidc-callback.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-oidc-env.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-opcode.c @@ -1092,6 +1093,7 @@ set (test-libmongoc-sources ${PROJECT_SOURCE_DIR}/tests/test-mongoc-max-staleness.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-mongos-pinning.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-oidc-callback.c + ${PROJECT_SOURCE_DIR}/tests/test-mongoc-oidc-cache.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-opts.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-primary-stepdown.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-queue.c diff --git a/src/libmongoc/src/mongoc/mongoc-oidc-cache-private.h b/src/libmongoc/src/mongoc/mongoc-oidc-cache-private.h new file mode 100644 index 00000000000..3c6f90f47b5 --- /dev/null +++ b/src/libmongoc/src/mongoc/mongoc-oidc-cache-private.h @@ -0,0 +1,66 @@ +/* + * Copyright 2009-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MONGOC_OIDC_CACHE_PRIVATE_H +#define MONGOC_OIDC_CACHE_PRIVATE_H + +#include +#include + +// mongoc_oidc_cache_t implements the OIDC spec "Client Cache". +// Stores the OIDC callback, cache, and lock. +// Expected to be shared among all clients in a pool. +typedef struct mongoc_oidc_cache_t mongoc_oidc_cache_t; + +mongoc_oidc_cache_t * +mongoc_oidc_cache_new(void); + +// mongoc_oidc_cache_set_callback sets the token callback. +// Not thread safe. Call before any authentication can occur. +void +mongoc_oidc_cache_set_callback(mongoc_oidc_cache_t *cache, const mongoc_oidc_callback_t *cb); + +// mongoc_oidc_cache_get_callback gets the token callback. +const mongoc_oidc_callback_t * +mongoc_oidc_cache_get_callback(const mongoc_oidc_cache_t *cache); + +// mongoc_oidc_cache_set_usleep_fn sets a custom sleep function. +// Not thread safe. Call before any authentication can occur. +void +mongoc_oidc_cache_set_usleep_fn(mongoc_oidc_cache_t *cache, mongoc_usleep_func_t usleep_fn, void *usleep_data); + +// mongoc_oidc_cache_get_token returns a token or NULL on error. Thread safe. +// Sets *found_in_cache to indicate if the returned token came from the cache or callback. +// Calls sleep if needed to enforce 100ms delay between calls to the callback. +char * +mongoc_oidc_cache_get_token(mongoc_oidc_cache_t *cache, bool *found_in_cache, bson_error_t *error); + +// mongoc_oidc_cache_get_cached_token returns a cached token or NULL if none is cached. Thread safe. +char * +mongoc_oidc_cache_get_cached_token(const mongoc_oidc_cache_t *cache); + +// mongoc_oidc_cache_set_cached_token overwrites the cached token. Useful for tests. Thread safe. +void +mongoc_oidc_cache_set_cached_token(mongoc_oidc_cache_t *cache, const char *token); + +// mongoc_oidc_cache_invalidate_token invalidates the cached token if it matches `token`. Thread safe. +void +mongoc_oidc_cache_invalidate_token(mongoc_oidc_cache_t *cache, const char *token); + +void +mongoc_oidc_cache_destroy(mongoc_oidc_cache_t *); + +#endif // MONGOC_OIDC_CACHE_PRIVATE_H diff --git a/src/libmongoc/src/mongoc/mongoc-oidc-cache.c b/src/libmongoc/src/mongoc/mongoc-oidc-cache.c new file mode 100644 index 00000000000..d32e04c96ba --- /dev/null +++ b/src/libmongoc/src/mongoc/mongoc-oidc-cache.c @@ -0,0 +1,229 @@ +/* + * Copyright 2009-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include // MC_DISABLE_CAST_QUAL_WARNING_BEGIN +#include +#include +#include +#include + +#include +#include + +#define SET_ERROR(...) _mongoc_set_error(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, __VA_ARGS__) + +struct mongoc_oidc_cache_t { + // callback is owned. NULL if unset. Not guarded by lock. Set before requesting tokens. + mongoc_oidc_callback_t *callback; + + // usleep_fn is used to sleep between calls to the callback. Not guarded by lock. Set before requesting tokens. + mongoc_usleep_func_t usleep_fn; + void *usleep_data; + + // lock is used to prevent concurrent calls to callback. Guards access to token, last_called, and ever_called. + bson_shared_mutex_t lock; + + // token is a cached OIDC access token. + char *token; + + // last_call tracks the time just after the last call to the callback. + mlib_time_point last_called; + + // ever_called is set to true after the first call to the callback. + bool ever_called; +}; + +mongoc_oidc_cache_t * +mongoc_oidc_cache_new(void) +{ + mongoc_oidc_cache_t *oidc = bson_malloc0(sizeof(mongoc_oidc_cache_t)); + oidc->usleep_fn = mongoc_usleep_default_impl; + bson_shared_mutex_init(&oidc->lock); + return oidc; +} + +void +mongoc_oidc_cache_set_callback(mongoc_oidc_cache_t *cache, const mongoc_oidc_callback_t *cb) +{ + BSON_ASSERT_PARAM(cache); + BSON_OPTIONAL_PARAM(cb); + + BSON_ASSERT(!cache->ever_called); + + if (cache->callback) { + mongoc_oidc_callback_destroy(cache->callback); + } + cache->callback = cb ? mongoc_oidc_callback_copy(cb) : NULL; +} + +const mongoc_oidc_callback_t * +mongoc_oidc_cache_get_callback(const mongoc_oidc_cache_t *cache) +{ + BSON_ASSERT_PARAM(cache); + + return cache->callback; +} + +void +mongoc_oidc_cache_set_usleep_fn(mongoc_oidc_cache_t *cache, mongoc_usleep_func_t usleep_fn, void *usleep_data) +{ + BSON_ASSERT_PARAM(cache); + BSON_OPTIONAL_PARAM(usleep_fn); + BSON_OPTIONAL_PARAM(usleep_data); + + BSON_ASSERT(!cache->ever_called); + + cache->usleep_fn = usleep_fn ? usleep_fn : mongoc_usleep_default_impl; + cache->usleep_data = usleep_data; +} + +void +mongoc_oidc_cache_destroy(mongoc_oidc_cache_t *cache) +{ + if (!cache) { + return; + } + bson_free(cache->token); + bson_shared_mutex_destroy(&cache->lock); + mongoc_oidc_callback_destroy(cache->callback); + bson_free(cache); +} + +char * +mongoc_oidc_cache_get_cached_token(const mongoc_oidc_cache_t *cache) +{ + BSON_ASSERT_PARAM(cache); + + // Cast away const to lock. This function is logically const (read-only). + MC_DISABLE_CAST_QUAL_WARNING_BEGIN + bson_shared_mutex_lock_shared(&((mongoc_oidc_cache_t *)cache)->lock); + char *token = bson_strdup(cache->token); + bson_shared_mutex_unlock_shared(&((mongoc_oidc_cache_t *)cache)->lock); + MC_DISABLE_CAST_QUAL_WARNING_END + return token; +} + +void +mongoc_oidc_cache_set_cached_token(mongoc_oidc_cache_t *cache, const char *token) +{ + BSON_ASSERT_PARAM(cache); + BSON_OPTIONAL_PARAM(token); + + char *old_token; + + // Lock to update token: + { + bson_shared_mutex_lock(&cache->lock); + old_token = cache->token; + cache->token = bson_strdup(token); + bson_shared_mutex_unlock(&cache->lock); + } + bson_free(old_token); +} + +char * +mongoc_oidc_cache_get_token(mongoc_oidc_cache_t *cache, bool *found_in_cache, bson_error_t *error) +{ + BSON_ASSERT_PARAM(cache); + BSON_ASSERT_PARAM(found_in_cache); + BSON_OPTIONAL_PARAM(error); + + char *token = NULL; + + *found_in_cache = false; + + if (!cache->callback) { + SET_ERROR("MONGODB-OIDC requested, but no callback set"); + return NULL; + } + + token = mongoc_oidc_cache_get_cached_token(cache); + if (NULL != token) { + *found_in_cache = true; + return token; + } + + // Prepare to call callback outside of lock: + mongoc_oidc_credential_t *cred = NULL; + mongoc_oidc_callback_params_t *params = mongoc_oidc_callback_params_new(); + mongoc_oidc_callback_params_set_user_data(params, mongoc_oidc_callback_get_user_data(cache->callback)); + // From spec: "If CSOT is not applied, then the driver MUST use 1 minute as the timeout." + // The timeout parameter (when set) is meant to be directly compared against bson_get_monotonic_time(). It is a + // time point, not a duration. + mongoc_oidc_callback_params_set_timeout( + params, mlib_microseconds_count(mlib_time_add(mlib_now(), mlib_duration(60, s)).time_since_monotonic_start)); + + // Obtain write-lock: + { + bson_shared_mutex_lock(&cache->lock); + // Check if another thread populated cache between checking cached token and obtaining write lock: + if (cache->token) { + *found_in_cache = true; + token = bson_strdup(cache->token); + goto unlock_and_return; + } + + // From spec: "Wait until it has been at least 100ms since the last callback invocation" + if (cache->ever_called) { + mlib_duration since_last_call = mlib_time_difference(mlib_now(), cache->last_called); + if (mlib_duration_cmp(since_last_call, <, (100, ms))) { + mlib_duration to_sleep = mlib_duration((100, ms), minus, since_last_call); + cache->usleep_fn(mlib_microseconds_count(to_sleep), cache->usleep_data); + } + } + + // Call callback: + cred = mongoc_oidc_callback_get_fn(cache->callback)(params); + + cache->last_called = mlib_now(); + cache->ever_called = true; + + if (!cred) { + SET_ERROR("MONGODB-OIDC callback failed"); + goto unlock_and_return; + } + + token = bson_strdup(mongoc_oidc_credential_get_access_token(cred)); + cache->token = bson_strdup(token); // Cache a copy. + + unlock_and_return: + bson_shared_mutex_unlock(&cache->lock); + } + mongoc_oidc_callback_params_destroy(params); + mongoc_oidc_credential_destroy(cred); + return token; +} + +void +mongoc_oidc_cache_invalidate_token(mongoc_oidc_cache_t *cache, const char *token) +{ + BSON_ASSERT_PARAM(cache); + BSON_ASSERT_PARAM(token); + + char *old_token = NULL; + + // Lock to clear token + { + bson_shared_mutex_lock(&cache->lock); + if (cache->token && 0 == strcmp(cache->token, token)) { + old_token = cache->token; + cache->token = NULL; + } + bson_shared_mutex_unlock(&cache->lock); + } + + bson_free(old_token); +} diff --git a/src/libmongoc/src/mongoc/mongoc-oidc-callback.c b/src/libmongoc/src/mongoc/mongoc-oidc-callback.c index 419f460eb44..817903beb81 100644 --- a/src/libmongoc/src/mongoc/mongoc-oidc-callback.c +++ b/src/libmongoc/src/mongoc/mongoc-oidc-callback.c @@ -28,7 +28,7 @@ struct _mongoc_oidc_callback_t { struct _mongoc_oidc_callback_params_t { void *user_data; char *username; - int64_t timeout; // Guarded by timeout_is_set. + int64_t timeout; // Guarded by timeout_is_set. In microseconds since monotonic clock start. int32_t version; bool cancelled_with_timeout; bool timeout_is_set; diff --git a/src/libmongoc/tests/test-libmongoc-main.c b/src/libmongoc/tests/test-libmongoc-main.c index 35944e8929d..5d1c319b7e7 100644 --- a/src/libmongoc/tests/test-libmongoc-main.c +++ b/src/libmongoc/tests/test-libmongoc-main.c @@ -160,6 +160,7 @@ main(int argc, char *argv[]) TEST_INSTALL(test_service_gcp_install); TEST_INSTALL(test_mcd_nsinfo_install); TEST_INSTALL(test_bulkwrite_install); + TEST_INSTALL(test_mongoc_oidc_install); TEST_INSTALL(test_mongoc_oidc_callback_install); TEST_INSTALL(test_secure_channel_install); TEST_INSTALL(test_stream_tracker_install); diff --git a/src/libmongoc/tests/test-mongoc-oidc-cache.c b/src/libmongoc/tests/test-mongoc-oidc-cache.c new file mode 100644 index 00000000000..16294b0ff32 --- /dev/null +++ b/src/libmongoc/tests/test-mongoc-oidc-cache.c @@ -0,0 +1,366 @@ +/* + * Copyright 2009-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +typedef struct { + int call_count; + bool returns_null; +} callback_ctx_t; + +#define PLACEHOLDER_TOKEN "PLACEHOLDER_TOKEN" + +static mongoc_oidc_credential_t * +oidc_callback_fn(mongoc_oidc_callback_params_t *params) +{ + callback_ctx_t *ctx = mongoc_oidc_callback_params_get_user_data(params); + ASSERT(ctx); + ctx->call_count += 1; + if (ctx->returns_null) { + return NULL; + } + return mongoc_oidc_credential_new(PLACEHOLDER_TOKEN); +} + +static void +test_oidc_cache_works(void) +{ + bool found_in_cache = false; + bson_error_t error; + + mongoc_oidc_cache_t *cache = mongoc_oidc_cache_new(); + callback_ctx_t ctx = {0}; + + // Expect error if no callback set: + { + ASSERT(!mongoc_oidc_cache_get_token(cache, &found_in_cache, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "no callback set"); + ASSERT(!mongoc_oidc_cache_get_cached_token(cache)); + } + + // Set a callback: + { + mongoc_oidc_callback_t *cb = mongoc_oidc_callback_new(oidc_callback_fn); + mongoc_oidc_callback_set_user_data(cb, &ctx); + mongoc_oidc_cache_set_callback(cache, cb); + mongoc_oidc_callback_destroy(cb); + } + + // Expect callback is called to fetch token: + { + char *token = mongoc_oidc_cache_get_token(cache, &found_in_cache, &error); + ASSERT_OR_PRINT(token, error); + ASSERT_CMPSTR(token, PLACEHOLDER_TOKEN); + ASSERT_CMPINT(ctx.call_count, ==, 1); + ASSERT(!found_in_cache); + bson_free(token); + } + + // Expect token is cached: + { + char *token = mongoc_oidc_cache_get_cached_token(cache); + ASSERT(token); + bson_free(token); + } + + // Expect callback is not called if token is cached: + { + char *token = mongoc_oidc_cache_get_token(cache, &found_in_cache, &error); + ASSERT_OR_PRINT(token, error); + ASSERT_CMPSTR(token, PLACEHOLDER_TOKEN); + ASSERT_CMPINT(ctx.call_count, ==, 1); + ASSERT(found_in_cache); + bson_free(token); + } + + // Invalidating a different token has no effect: + { + mongoc_oidc_cache_invalidate_token(cache, "different-token"); + char *token = mongoc_oidc_cache_get_cached_token(cache); + ASSERT(token); + bson_free(token); + } + + // Invalidating same token clears cache: + { + char *token = mongoc_oidc_cache_get_cached_token(cache); + ASSERT(token); + mongoc_oidc_cache_invalidate_token(cache, token); + bson_free(token); + ASSERT(!mongoc_oidc_cache_get_cached_token(cache)); + } + + mongoc_oidc_cache_destroy(cache); +} + +static void +test_oidc_cache_waits_between_calls(void) +{ + bool found_in_cache = false; + bson_error_t error; + mongoc_oidc_cache_t *cache = mongoc_oidc_cache_new(); + callback_ctx_t ctx = {0}; + + // Set a callback: + { + mongoc_oidc_callback_t *cb = mongoc_oidc_callback_new(oidc_callback_fn); + mongoc_oidc_callback_set_user_data(cb, &ctx); + mongoc_oidc_cache_set_callback(cache, cb); + mongoc_oidc_callback_destroy(cb); + } + + mlib_time_point const start = mlib_now(); + + // Expect callback is called to fetch token: + { + char *token = mongoc_oidc_cache_get_token(cache, &found_in_cache, &error); + ASSERT_OR_PRINT(token, error); + ASSERT_CMPSTR(token, PLACEHOLDER_TOKEN); + ASSERT_CMPINT(ctx.call_count, ==, 1); + ASSERT(!found_in_cache); + bson_free(token); + } + + // Invalidate token to clear cache: + { + char *token = mongoc_oidc_cache_get_cached_token(cache); + ASSERT(token); + mongoc_oidc_cache_invalidate_token(cache, token); + bson_free(token); + ASSERT(!mongoc_oidc_cache_get_cached_token(cache)); + } + + const int64_t expected_delay = 90; // Use shorter time. Windows appears to sleep slightly less. + // Expect duration less than delay: + { + mlib_duration diff = mlib_time_difference(mlib_now(), start); + ASSERT_CMPINT64(mlib_milliseconds_count(diff), <, expected_delay); + } + + // Fetch token again: + { + char *token = mongoc_oidc_cache_get_token(cache, &found_in_cache, &error); + ASSERT_OR_PRINT(token, error); + ASSERT_CMPSTR(token, PLACEHOLDER_TOKEN); + ASSERT_CMPINT(ctx.call_count, ==, 2); + ASSERT(!found_in_cache); + bson_free(token); + } + + // Expect delay: + { + mlib_duration diff = mlib_time_difference(mlib_now(), start); + ASSERT_CMPINT64(mlib_milliseconds_count(diff), >=, expected_delay); + } + + mongoc_oidc_cache_destroy(cache); +} + +static void +test_oidc_cache_set_callback(void) +{ + mongoc_oidc_cache_t *cache = mongoc_oidc_cache_new(); + + ASSERT(!mongoc_oidc_cache_get_callback(cache)); + + // Can set a callback: + { + mongoc_oidc_callback_t *cb = mongoc_oidc_callback_new(oidc_callback_fn); + mongoc_oidc_cache_set_callback(cache, cb); + ASSERT(mongoc_oidc_callback_get_fn(mongoc_oidc_cache_get_callback(cache)) == oidc_callback_fn); + mongoc_oidc_callback_destroy(cb); + } + + // Can clear a callback: + { + mongoc_oidc_cache_set_callback(cache, NULL); + ASSERT(!mongoc_oidc_cache_get_callback(cache)); + } + + mongoc_oidc_cache_destroy(cache); +} + +typedef struct { + int call_count; + int64_t last_arg; +} sleep_ctx_t; + +static void +sleep_callback_fn(int64_t usec, void *user_data) +{ + ASSERT(user_data); + sleep_ctx_t *ctx = (sleep_ctx_t *)user_data; + ctx->call_count += 1; + ctx->last_arg = usec; +} + +static void +test_oidc_cache_set_sleep(void) +{ + callback_ctx_t ctx = {0}; + sleep_ctx_t sleep_ctx = {0}; + mongoc_oidc_cache_t *cache = mongoc_oidc_cache_new(); + + // Set a callback to test: + { + mongoc_oidc_callback_t *cb = mongoc_oidc_callback_new(oidc_callback_fn); + mongoc_oidc_callback_set_user_data(cb, &ctx); + mongoc_oidc_cache_set_callback(cache, cb); + mongoc_oidc_callback_destroy(cb); + } + + // Can use a custom sleep function: + { + bool found_in_cache = false; + bson_error_t error; + char *token; + + // Set a custom sleep function: + mongoc_oidc_cache_set_usleep_fn(cache, sleep_callback_fn, &sleep_ctx); + + // First call to get_token does not sleep: + token = mongoc_oidc_cache_get_token(cache, &found_in_cache, &error); + ASSERT_OR_PRINT(token, error); + ASSERT_CMPSTR(token, PLACEHOLDER_TOKEN); + ASSERT_CMPINT(ctx.call_count, ==, 1); + ASSERT_CMPINT(sleep_ctx.call_count, ==, 0); + ASSERT(!found_in_cache); + + // Invalidate cache to trigger another call: + mongoc_oidc_cache_invalidate_token(cache, token); + bson_free(token); + + // Second call to get_token sleeps to ensure at least 100ms between calls: + token = mongoc_oidc_cache_get_token(cache, &found_in_cache, &error); + ASSERT_OR_PRINT(token, error); + ASSERT_CMPSTR(token, PLACEHOLDER_TOKEN); + ASSERT_CMPINT(ctx.call_count, ==, 2); + ASSERT_CMPINT(sleep_ctx.call_count, ==, 1); + ASSERT_CMPINT64(sleep_ctx.last_arg, >, 0); + ASSERT_CMPINT64(sleep_ctx.last_arg, <=, 100 * 1000); // at most 100ms + ASSERT(!found_in_cache); + bson_free(token); + } + + mongoc_oidc_cache_destroy(cache); +} + +static void +test_oidc_cache_set_cached_token(void) +{ + mongoc_oidc_cache_t *cache = mongoc_oidc_cache_new(); + + ASSERT(!mongoc_oidc_cache_get_cached_token(cache)); + + // Can set a cached token: + { + mongoc_oidc_cache_set_cached_token(cache, PLACEHOLDER_TOKEN); + char *got = mongoc_oidc_cache_get_cached_token(cache); + ASSERT_CMPSTR(got, PLACEHOLDER_TOKEN); + bson_free(got); + } + + // Can clear cached token: + { + mongoc_oidc_cache_set_cached_token(cache, NULL); + ASSERT(!mongoc_oidc_cache_get_cached_token(cache)); + } + + mongoc_oidc_cache_destroy(cache); +} + +static void +test_oidc_cache_propagates_error(void) +{ + // Test a callback returning NULL. + bool found_in_cache = false; + bson_error_t error; + + mongoc_oidc_cache_t *cache = mongoc_oidc_cache_new(); + callback_ctx_t ctx = {.returns_null = true}; + + // Set a callback: + { + mongoc_oidc_callback_t *cb = mongoc_oidc_callback_new(oidc_callback_fn); + mongoc_oidc_callback_set_user_data(cb, &ctx); + mongoc_oidc_cache_set_callback(cache, cb); + mongoc_oidc_callback_destroy(cb); + } + + // Expect error: + { + ASSERT(!mongoc_oidc_cache_get_token(cache, &found_in_cache, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "callback failed"); + ASSERT(!found_in_cache); + ASSERT(!mongoc_oidc_cache_get_cached_token(cache)); + } + + mongoc_oidc_cache_destroy(cache); +} + +static void +test_oidc_cache_invalidate(void) +{ + mongoc_oidc_cache_t *cache = mongoc_oidc_cache_new(); + + // Can invalidate when nothing cached: + { + ASSERT(!mongoc_oidc_cache_get_cached_token(cache)); + mongoc_oidc_cache_invalidate_token(cache, "foobar"); + ASSERT(!mongoc_oidc_cache_get_cached_token(cache)); + } + + // Cache a token: + { + mongoc_oidc_cache_set_cached_token(cache, "foo"); + char *token = mongoc_oidc_cache_get_cached_token(cache); + ASSERT_CMPSTR(token, "foo"); + bson_free(token); + } + + // Invalidating a different token has no effect: + { + mongoc_oidc_cache_invalidate_token(cache, "bar"); + char *token = mongoc_oidc_cache_get_cached_token(cache); + ASSERT_CMPSTR(token, "foo"); + bson_free(token); + } + + // Invalidating same token clears cache: + { + mongoc_oidc_cache_invalidate_token(cache, "foo"); + ASSERT(!mongoc_oidc_cache_get_cached_token(cache)); + } + + mongoc_oidc_cache_destroy(cache); +} + + +void +test_mongoc_oidc_install(TestSuite *suite) +{ + TestSuite_Add(suite, "/oidc/cache/works", test_oidc_cache_works); + TestSuite_Add(suite, "/oidc/cache/set_callback", test_oidc_cache_set_callback); + TestSuite_Add(suite, "/oidc/cache/set_sleep", test_oidc_cache_set_sleep); + TestSuite_Add(suite, "/oidc/cache/set_cached_token", test_oidc_cache_set_cached_token); + TestSuite_Add(suite, "/oidc/cache/propagates_error", test_oidc_cache_propagates_error); + TestSuite_Add(suite, "/oidc/cache/invalidate", test_oidc_cache_invalidate); + TestSuite_Add(suite, "/oidc/cache/waits_between_calls", test_oidc_cache_waits_between_calls); +}