Skip to main content

alibi_plugins/api_key/
mod.rs

1mod cleanup;
2mod config;
3
4pub use config::ApiKeyConfig;
5pub use config::ApiKeyReferences;
6pub use config::KeyExpirationConfig;
7pub use config::RateLimitDefaults;
8mod callbacks;
9mod secondary_usage;
10mod storage;
11pub use storage::{ApiKeyStorage, ApiKeyStorageMode};
12mod endpoint;
13pub use endpoint::{
14    ApiKeyVerificationInput, ApiKeyVerificationOutput, DeleteKeyResponse, GetKeyQuery,
15};
16
17pub(super) mod handlers;
18
19pub(super) mod types;
20
21mod verification;
22
23use alibi_core::entity::AuthUser;
24use alibi_core::{AuthContext, AuthError, AuthResult, BeforeRequestAction};
25use alibi_core::{AuthRequest, AuthResponse};
26use base64::Engine;
27use base64::engine::general_purpose::URL_SAFE_NO_PAD;
28pub use callbacks::{
29    ApiKeyCallbackContext, ApiKeyDefaultPermissions, ApiKeyGenerationOptions, ApiKeyGenerator,
30    ApiKeyGetter, ApiKeyPermissions, ApiKeyValidator,
31};
32use handlers::{create_key_core, delete_key_core, get_key_core, list_keys_core, update_key_core};
33use rand::seq::IndexedRandom;
34use sha2::{Digest, Sha256};
35use std::sync::{Arc, Mutex};
36use types::parse_api_key_body;
37pub use types::{
38    CreateKeyRequest, CreateKeyResponse, DeleteExpiredApiKeysResponse, DeleteKeyRequest,
39    ListKeysQuery, ListKeysResponse, UpdateKeyRequest,
40};
41pub use verification::{
42    ApiKeyErrorDetails, ApiKeyErrorMessage, ApiKeyValidationError, ApiKeyVerificationError,
43    VerifyApiKey,
44};
45
46/// Dedicated API Key error codes aligned with the TypeScript `API_KEY_ERROR_CODES`.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ApiKeyErrorCode {
49    InvalidApiKey,
50    KeyDisabled,
51    KeyExpired,
52    UsageExceeded,
53    KeyNotFound,
54    RateLimited,
55    UnauthorizedSession,
56    InvalidPrefixLength,
57    InvalidNameLength,
58    MetadataDisabled,
59    NoValuesToUpdate,
60    KeyDisabledExpiration,
61    ExpiresInTooSmall,
62    ExpiresInTooLarge,
63    InvalidRemaining,
64    RefillAmountAndIntervalRequired,
65    RefillIntervalAndAmountRequired,
66    NameRequired,
67    InvalidUserIdFromApiKey,
68    InvalidReferenceIdFromApiKey,
69    NoDefaultConfiguration,
70    OrganizationIdRequired,
71    OrganizationPluginRequired,
72    UserNotMemberOfOrganization,
73    InsufficientApiKeyPermissions,
74    ServerOnlyProperty,
75    FailedToUpdateApiKey,
76    InvalidMetadataType,
77}
78
79impl ApiKeyErrorCode {
80    #[must_use]
81    pub const fn as_str(self) -> &'static str {
82        match self {
83            Self::InvalidApiKey => "INVALID_API_KEY",
84            Self::KeyDisabled => "KEY_DISABLED",
85            Self::KeyExpired => "KEY_EXPIRED",
86            Self::UsageExceeded => "USAGE_EXCEEDED",
87            Self::KeyNotFound => "KEY_NOT_FOUND",
88            Self::RateLimited => "RATE_LIMITED",
89            Self::UnauthorizedSession => "UNAUTHORIZED_SESSION",
90            Self::InvalidPrefixLength => "INVALID_PREFIX_LENGTH",
91            Self::InvalidNameLength => "INVALID_NAME_LENGTH",
92            Self::MetadataDisabled => "METADATA_DISABLED",
93            Self::NoValuesToUpdate => "NO_VALUES_TO_UPDATE",
94            Self::KeyDisabledExpiration => "KEY_DISABLED_EXPIRATION",
95            Self::ExpiresInTooSmall => "EXPIRES_IN_IS_TOO_SMALL",
96            Self::ExpiresInTooLarge => "EXPIRES_IN_IS_TOO_LARGE",
97            Self::InvalidRemaining => "INVALID_REMAINING",
98            Self::RefillAmountAndIntervalRequired => "REFILL_AMOUNT_AND_INTERVAL_REQUIRED",
99            Self::RefillIntervalAndAmountRequired => "REFILL_INTERVAL_AND_AMOUNT_REQUIRED",
100            Self::NameRequired => "NAME_REQUIRED",
101            Self::InvalidUserIdFromApiKey => "INVALID_USER_ID_FROM_API_KEY",
102            Self::InvalidReferenceIdFromApiKey => "INVALID_REFERENCE_ID_FROM_API_KEY",
103            Self::NoDefaultConfiguration => "NO_DEFAULT_API_KEY_CONFIGURATION_FOUND",
104            Self::OrganizationIdRequired => "ORGANIZATION_ID_REQUIRED",
105            Self::OrganizationPluginRequired => "ORGANIZATION_PLUGIN_REQUIRED",
106            Self::UserNotMemberOfOrganization => "USER_NOT_MEMBER_OF_ORGANIZATION",
107            Self::InsufficientApiKeyPermissions => "INSUFFICIENT_API_KEY_PERMISSIONS",
108            Self::ServerOnlyProperty => "SERVER_ONLY_PROPERTY",
109            Self::FailedToUpdateApiKey => "FAILED_TO_UPDATE_API_KEY",
110            Self::InvalidMetadataType => "INVALID_METADATA_TYPE",
111        }
112    }
113
114    #[must_use]
115    pub const fn message(self) -> &'static str {
116        match self {
117            Self::InvalidApiKey => "Invalid API key.",
118            Self::KeyDisabled => "API Key is disabled",
119            Self::KeyExpired => "API Key has expired",
120            Self::UsageExceeded => "API Key has reached its usage limit",
121            Self::KeyNotFound => "API Key not found",
122            Self::RateLimited => "Rate limit exceeded.",
123            Self::UnauthorizedSession => "Unauthorized or invalid session",
124            Self::InvalidPrefixLength => "The prefix length is either too large or too small.",
125            Self::InvalidNameLength => "The name length is either too large or too small.",
126            Self::MetadataDisabled => "Metadata is disabled.",
127            Self::NoValuesToUpdate => "No values to update.",
128            Self::KeyDisabledExpiration => "Custom key expiration values are disabled.",
129            Self::ExpiresInTooSmall => {
130                "The expiresIn is smaller than the predefined minimum value."
131            }
132            Self::ExpiresInTooLarge => "The expiresIn is larger than the predefined maximum value.",
133            Self::InvalidRemaining => "The remaining count is either too large or too small.",
134            Self::RefillAmountAndIntervalRequired => {
135                "refillAmount is required when refillInterval is provided"
136            }
137            Self::RefillIntervalAndAmountRequired => {
138                "refillInterval is required when refillAmount is provided"
139            }
140            Self::NameRequired => "API Key name is required.",
141            Self::InvalidUserIdFromApiKey => "The user id from the API key is invalid.",
142            Self::InvalidReferenceIdFromApiKey => "The reference id from the API key is invalid.",
143            Self::NoDefaultConfiguration => "No default api-key configuration found.",
144            Self::OrganizationIdRequired => {
145                "Organization ID is required for organization-owned API keys."
146            }
147            Self::OrganizationPluginRequired => {
148                "Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
149            }
150            Self::UserNotMemberOfOrganization => {
151                "You are not a member of the organization that owns this API key."
152            }
153            Self::InsufficientApiKeyPermissions => {
154                "You do not have permission to perform this action on organization API keys."
155            }
156            Self::ServerOnlyProperty => {
157                "The property you're trying to set can only be set from the server auth instance only."
158            }
159            Self::FailedToUpdateApiKey => "Failed to update API key",
160            Self::InvalidMetadataType => "metadata must be an object or undefined",
161        }
162    }
163}
164
165impl serde::Serialize for ApiKeyErrorCode {
166    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
167        serializer.serialize_str(self.as_str())
168    }
169}
170
171// Pinned api-key module state is shared by all plugin instances and databases.
172static LAST_EXPIRED_CHECK: Mutex<Option<i64>> = Mutex::new(None);
173
174/// API Key management plugin.
175#[derive(Clone)]
176pub struct ApiKeyPlugin {
177    /// Registered configurations. Unscoped requests use the default configuration.
178    pub(super) configurations: Vec<ApiKeyConfig>,
179}
180
181impl ApiKeyPlugin {
182    /// Register an additional named configuration.
183    ///
184    /// Upstream allows several api-key configurations side by side, each with
185    /// its own `config_id`, ownership model and limits.
186    #[must_use]
187    pub fn configuration(mut self, config: ApiKeyConfig) -> Self {
188        self.configurations.push(config.normalized());
189        self
190    }
191
192    /// Pick the configuration a request addressed, mirroring upstream's
193    /// `resolveConfiguration`: an unknown or absent `config_id` falls back to
194    /// the default one, and a missing default is a client error.
195    pub(super) fn resolve_configuration(
196        &self,
197        config_id: Option<&str>,
198    ) -> AuthResult<&ApiKeyConfig> {
199        if let Some(config_id) = config_id
200            && let Some(found) = self
201                .configurations
202                .iter()
203                .find(|config| config.config_id == config_id)
204        {
205            return Ok(found);
206        }
207
208        self.configurations
209            .iter()
210            .find(|config| is_default_config_id(&config.config_id))
211            .ok_or_else(|| api_key_error(ApiKeyErrorCode::NoDefaultConfiguration))
212    }
213}
214
215/// Builder for [`ApiKeyPlugin`] powered by the `bon` crate.
216///
217/// Usage:
218/// ```
219/// use alibi_plugins::api_key::{ApiKeyPlugin, RateLimitDefaults};
220/// let plugin = ApiKeyPlugin::builder()
221///     .key_length(48)
222///     .prefix("ba_".to_string())
223///     .enable_metadata(true)
224///     .rate_limit(RateLimitDefaults { enabled: true, time_window: 60_000.0, max_requests: 5.0 })
225///     .build();
226/// ```
227#[bon::bon]
228impl ApiKeyPlugin {
229    #[builder]
230    #[must_use]
231    pub fn new(
232        #[builder(default = "default".to_owned())] config_id: String,
233        #[builder(default)] references: ApiKeyReferences,
234        #[builder(default)] storage: ApiKeyStorageMode,
235        #[builder(default)] fallback_to_database: bool,
236        custom_storage: Option<Arc<dyn ApiKeyStorage>>,
237        secondary_storage: Option<Arc<dyn alibi_core::store::CacheAdapter>>,
238        #[builder(default = 64.0, into)] key_length: f64,
239        prefix: Option<String>,
240        default_permissions: Option<ApiKeyPermissions>,
241        custom_key_generator: Option<Arc<dyn ApiKeyGenerator>>,
242        default_permissions_callback: Option<Arc<dyn ApiKeyDefaultPermissions>>,
243        #[builder(default = vec!["x-api-key".to_owned()])] api_key_headers: Vec<String>,
244        custom_api_key_getter: Option<Arc<dyn ApiKeyGetter>>,
245        custom_api_key_validator: Option<Arc<dyn ApiKeyValidator>>,
246        #[builder(default = false)] disable_key_hashing: bool,
247        #[builder(default = 6.0, into)] starting_characters_length: f64,
248        #[builder(default = true)] store_starting_characters: bool,
249        #[builder(default = 32.0, into)] max_prefix_length: f64,
250        #[builder(default = 1.0, into)] min_prefix_length: f64,
251        #[builder(default = 32.0, into)] max_name_length: f64,
252        #[builder(default = 1.0, into)] min_name_length: f64,
253        #[builder(default = false)] require_name: bool,
254        #[builder(default = false)] enable_metadata: bool,
255        #[builder(default)] key_expiration: KeyExpirationConfig,
256        #[builder(default)] rate_limit: RateLimitDefaults,
257        #[builder(default = false)] enable_session_for_api_keys: bool,
258        #[builder(default = false)] defer_updates: bool,
259    ) -> Self {
260        Self {
261            configurations: vec![
262                ApiKeyConfig {
263                    config_id,
264                    references,
265                    storage,
266                    fallback_to_database,
267                    secondary_storage,
268                    custom_storage,
269                    key_length,
270                    prefix,
271                    default_permissions,
272                    custom_key_generator,
273                    default_permissions_callback,
274                    api_key_headers,
275                    custom_api_key_getter,
276                    custom_api_key_validator,
277                    disable_key_hashing,
278                    starting_characters_length,
279                    store_starting_characters,
280                    max_prefix_length,
281                    min_prefix_length,
282                    max_name_length,
283                    min_name_length,
284                    require_name,
285                    enable_metadata,
286                    key_expiration,
287                    rate_limit,
288                    enable_session_for_api_keys,
289                    defer_updates,
290                }
291                .normalized(),
292            ],
293        }
294    }
295
296    #[must_use]
297    pub fn with_config(config: ApiKeyConfig) -> Self {
298        Self {
299            configurations: vec![config.normalized()],
300        }
301    }
302
303    pub(super) fn generate_key(
304        config: &ApiKeyConfig,
305        custom_prefix: Option<&str>,
306    ) -> AuthResult<(String, String, alibi_core::ApiKeyStartingCharacters)> {
307        // Match TS: generateRandomString(length, "a-z", "A-Z") — alpha only
308        const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
309        if config.key_length <= 0.0 {
310            return Err(AuthError::internal("Length must be a positive integer."));
311        }
312        // Source's zero-byte random buffer cannot advance positive lengths
313        // below one half. Nonfinite built-in lengths also cannot terminate.
314        if !config.key_length.is_finite() || config.key_length < 0.5 {
315            return Err(AuthError::internal("Unsupported random key length"));
316        }
317        let capacity = config
318            .key_length
319            .ceil()
320            .to_string()
321            .parse::<u32>()
322            .map_err(|error| AuthError::internal(error.to_string()))?;
323        let mut rng = rand::rng();
324        let mut raw = String::new();
325        raw.try_reserve_exact(
326            usize::try_from(capacity).map_err(|error| AuthError::internal(error.to_string()))?,
327        )
328        .map_err(|error| AuthError::internal(error.to_string()))?;
329        while f64::from(
330            u32::try_from(raw.len()).map_err(|error| AuthError::internal(error.to_string()))?,
331        ) < config.key_length
332        {
333            raw.push(ALPHABET.choose(&mut rng).copied().map_or('a', char::from));
334        }
335
336        let prefix = custom_prefix
337            .filter(|prefix| !prefix.is_empty())
338            .or(config.prefix.as_deref())
339            .unwrap_or("");
340        let full_key = format!("{prefix}{raw}");
341
342        // TS computes start from the full key (including prefix):
343        //   start = key.substring(0, charactersLength)
344        let start = Self::starting_characters(&full_key, config.starting_characters_length);
345
346        let hash = if config.disable_key_hashing {
347            full_key.clone()
348        } else {
349            Self::hash_key(&full_key)
350        };
351
352        Ok((full_key, hash, start))
353    }
354
355    pub(super) fn starting_characters(
356        key: &str,
357        length: f64,
358    ) -> alibi_core::ApiKeyStartingCharacters {
359        // substring(0, length) clamps negatives/NaN to zero, truncates finite
360        // fractions, and preserves all code units for positive infinity.
361        let end = if length.is_nan() || length < 0.0 {
362            0.0
363        } else {
364            length.trunc()
365        };
366        let units = key
367            .encode_utf16()
368            .scan(0_u32, |position, unit| {
369                let keep = f64::from(*position) < end;
370                *position = position.saturating_add(1);
371                keep.then_some(unit)
372            })
373            .collect();
374        alibi_core::ApiKeyStartingCharacters::from_utf16(units)
375    }
376
377    pub(super) fn hash_key(key: &str) -> String {
378        let mut hasher = Sha256::new();
379        hasher.update(key.as_bytes());
380        let digest = hasher.finalize();
381        URL_SAFE_NO_PAD.encode(digest)
382    }
383
384    pub(super) fn validate_prefix(config: &ApiKeyConfig, prefix: Option<&str>) -> AuthResult<()> {
385        if let Some(p) = prefix.filter(|prefix| !prefix.is_empty()) {
386            let len = f64::from(
387                u32::try_from(p.encode_utf16().count())
388                    .map_err(|error| AuthError::internal(error.to_string()))?,
389            );
390            if len < config.min_prefix_length || len > config.max_prefix_length {
391                return Err(api_key_error(ApiKeyErrorCode::InvalidPrefixLength));
392            }
393        }
394        Ok(())
395    }
396
397    /// Validate the `name` field.
398    ///
399    /// When `is_create` is true, `require_name` is enforced (name must be
400    /// present).  On updates `require_name` is **not** enforced -- the
401    /// caller may be updating unrelated fields without resending the name.
402    pub(super) fn validate_name(
403        config: &ApiKeyConfig,
404        name: Option<&str>,
405        is_create: bool,
406    ) -> AuthResult<()> {
407        if is_create && config.require_name && name.is_none_or(str::is_empty) {
408            return Err(api_key_error(ApiKeyErrorCode::NameRequired));
409        }
410        if let Some(n) = name.filter(|name| !is_create || !name.is_empty()) {
411            let len = f64::from(
412                u32::try_from(n.encode_utf16().count())
413                    .map_err(|error| AuthError::internal(error.to_string()))?,
414            );
415            if len < config.min_name_length || len > config.max_name_length {
416                return Err(api_key_error(ApiKeyErrorCode::InvalidNameLength));
417            }
418        }
419        Ok(())
420    }
421
422    pub(super) fn validate_expires_in(
423        config: &ApiKeyConfig,
424        expires_in: Option<f64>,
425    ) -> AuthResult<Option<f64>> {
426        let cfg = &config.key_expiration;
427        if let Some(secs) = expires_in {
428            if cfg.disable_custom_expires_time {
429                return Err(api_key_error(ApiKeyErrorCode::KeyDisabledExpiration));
430            }
431            // expiresIn is in seconds; min/max are in days
432            let days = secs / 86_400.0;
433            if days < cfg.min_expires_in {
434                return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooSmall));
435            }
436            if days > cfg.max_expires_in {
437                return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooLarge));
438            }
439            Ok(Some(secs))
440        } else {
441            Ok(cfg.default_expires_in)
442        }
443    }
444
445    pub(super) fn validate_metadata(
446        config: &ApiKeyConfig,
447        metadata: Option<&alibi_core::utils::json::JsValue>,
448    ) -> AuthResult<()> {
449        if let Some(value) = metadata.filter(|value| match value {
450            alibi_core::utils::json::JsValue::Null => false,
451            alibi_core::utils::json::JsValue::Bool(value) => *value,
452            alibi_core::utils::json::JsValue::Number(value) => *value != 0.0 && !value.is_nan(),
453            alibi_core::utils::json::JsValue::String(value) => !value.is_empty(),
454            alibi_core::utils::json::JsValue::Array(_)
455            | alibi_core::utils::json::JsValue::Object(_) => true,
456        }) {
457            if !config.enable_metadata {
458                return Err(api_key_error(ApiKeyErrorCode::MetadataDisabled));
459            }
460            if !value.is_object() && !value.is_array() {
461                return Err(api_key_error(ApiKeyErrorCode::InvalidMetadataType));
462            }
463        }
464        Ok(())
465    }
466
467    pub(super) fn validate_refill(
468        refill_interval: Option<f64>,
469        refill_amount: Option<f64>,
470    ) -> AuthResult<()> {
471        match (
472            refill_interval.filter(|value| *value != 0.0),
473            refill_amount.filter(|value| *value != 0.0),
474        ) {
475            (None, Some(_)) => Err(api_key_error(
476                ApiKeyErrorCode::RefillAmountAndIntervalRequired,
477            )),
478            (Some(_), None) => Err(api_key_error(
479                ApiKeyErrorCode::RefillIntervalAndAmountRequired,
480            )),
481            _ => Ok(()),
482        }
483    }
484
485    async fn handle_create(
486        &self,
487        req: &AuthRequest,
488        ctx: &AuthContext<impl alibi_core::AuthSchema>,
489    ) -> AuthResult<AuthResponse> {
490        let mut resolution = req.clone();
491        _ = resolution
492            .query
493            .insert("disableCookieCache".into(), "true".into());
494        let (user, _session) = ctx
495            .require_cached_session(&resolution)
496            .await
497            .map_err(|error| match error {
498                AuthError::Unauthenticated => api_key_error(ApiKeyErrorCode::UnauthorizedSession),
499                error @ (AuthError::Api { .. }
500                | AuthError::Upstream { .. }
501                | AuthError::BadRequest(_)
502                | AuthError::InvalidRequest(_)
503                | AuthError::Validation(_)
504                | AuthError::InvalidCredentials
505                | AuthError::AuthenticationFailed(_)
506                | AuthError::SessionNotFound
507                | AuthError::Forbidden(_)
508                | AuthError::UserCreationCancelled
509                | AuthError::SessionCreationCancelled
510                | AuthError::BannedUser(_)
511                | AuthError::Unauthorized
512                | AuthError::UserNotFound
513                | AuthError::NotFound(_)
514                | AuthError::Conflict(_)
515                | AuthError::MethodNotAllowed(_)
516                | AuthError::PayloadTooLarge(_)
517                | AuthError::UnprocessableEntity(_)
518                | AuthError::RateLimited { .. }
519                | AuthError::NotImplemented(_)
520                | AuthError::Config(_)
521                | AuthError::Database(_)
522                | AuthError::Serialization(_)
523                | AuthError::Plugin { .. }
524                | AuthError::CallbackFailure(_)
525                | AuthError::Internal(_)
526                | AuthError::Encryption(_)
527                | AuthError::PasswordHash(_)
528                | AuthError::Jwt(_)) => error,
529            })?;
530        let body: CreateKeyRequest = match parse_api_key_body(req) {
531            Ok(v) => v,
532            Err(resp) => return Ok(resp),
533        };
534        let response = match create_key_core(&body, user.id(), self, ctx, Some(req)).await {
535            Ok(response) => response,
536            Err(error)
537                if error.status_code() >= 500
538                    && !matches!(error, AuthError::Upstream { .. } | AuthError::Api { .. }) =>
539            {
540                return Ok(AuthResponse::new(500));
541            }
542            Err(error) => return Err(error),
543        };
544        Ok(AuthResponse::json(200, &response)?)
545    }
546
547    async fn handle_get(
548        &self,
549        req: &AuthRequest,
550        ctx: &AuthContext<impl alibi_core::AuthSchema>,
551    ) -> AuthResult<AuthResponse> {
552        let mut resolution = req.clone();
553        _ = resolution.query.remove("disableCookieCache");
554        _ = resolution.query.remove("disableRefresh");
555        let (user, _session) = super::helpers::ordinary_session(&resolution, ctx).await?;
556        let id = req
557            .query
558            .get("id")
559            .ok_or_else(|| AuthError::bad_request("Query parameter 'id' is required"))?;
560        let config_id = req.query.get("configId").map(String::as_str);
561        key_json_response(get_key_core(id, config_id, user.id(), self, ctx).await)
562    }
563
564    async fn handle_list(
565        &self,
566        req: &AuthRequest,
567        ctx: &AuthContext<impl alibi_core::AuthSchema>,
568    ) -> AuthResult<AuthResponse> {
569        let mut resolution = req.clone();
570        _ = resolution.query.remove("disableCookieCache");
571        _ = resolution.query.remove("disableRefresh");
572        let (user, _session) = super::helpers::ordinary_session(&resolution, ctx).await?;
573        let query = match ListKeysQuery::from_request(req) {
574            Ok(query) => query,
575            Err(response) => return Ok(response),
576        };
577        key_json_response(list_keys_core(user.id(), &query, self, ctx).await)
578    }
579
580    async fn handle_update(
581        &self,
582        req: &AuthRequest,
583        ctx: &AuthContext<impl alibi_core::AuthSchema>,
584    ) -> AuthResult<AuthResponse> {
585        let mut resolution = req.clone();
586        _ = resolution
587            .query
588            .insert("disableCookieCache".into(), "true".into());
589        let (user, _session) = ctx
590            .require_cached_session(&resolution)
591            .await
592            .map_err(|error| match error {
593                AuthError::Unauthenticated => api_key_error(ApiKeyErrorCode::UnauthorizedSession),
594                error @ (AuthError::Api { .. }
595                | AuthError::Upstream { .. }
596                | AuthError::BadRequest(_)
597                | AuthError::InvalidRequest(_)
598                | AuthError::Validation(_)
599                | AuthError::InvalidCredentials
600                | AuthError::AuthenticationFailed(_)
601                | AuthError::SessionNotFound
602                | AuthError::Forbidden(_)
603                | AuthError::UserCreationCancelled
604                | AuthError::SessionCreationCancelled
605                | AuthError::BannedUser(_)
606                | AuthError::Unauthorized
607                | AuthError::UserNotFound
608                | AuthError::NotFound(_)
609                | AuthError::Conflict(_)
610                | AuthError::MethodNotAllowed(_)
611                | AuthError::PayloadTooLarge(_)
612                | AuthError::UnprocessableEntity(_)
613                | AuthError::RateLimited { .. }
614                | AuthError::NotImplemented(_)
615                | AuthError::Config(_)
616                | AuthError::Database(_)
617                | AuthError::Serialization(_)
618                | AuthError::Plugin { .. }
619                | AuthError::CallbackFailure(_)
620                | AuthError::Internal(_)
621                | AuthError::Encryption(_)
622                | AuthError::PasswordHash(_)
623                | AuthError::Jwt(_)) => error,
624            })?;
625        let body: UpdateKeyRequest = match parse_api_key_body(req) {
626            Ok(v) => v,
627            Err(resp) => return Ok(resp),
628        };
629        let response = update_key_core(&body, user.id(), self, ctx).await?;
630        Ok(AuthResponse::json(200, &response)?)
631    }
632
633    async fn handle_delete(
634        &self,
635        req: &AuthRequest,
636        ctx: &AuthContext<impl alibi_core::AuthSchema>,
637    ) -> AuthResult<AuthResponse> {
638        let (user, _session) = super::helpers::ordinary_session(req, ctx).await?;
639        if user.banned() {
640            return Err(AuthError::authentication_failed("User is banned"));
641        }
642        let body: DeleteKeyRequest = match parse_api_key_body(req) {
643            Ok(v) => v,
644            Err(resp) => return Ok(resp),
645        };
646        let response = delete_key_core(&body, user.id(), self, ctx).await?;
647        Ok(AuthResponse::json(200, &response)?)
648    }
649}
650
651fn key_json_response<T: serde::Serialize>(result: AuthResult<T>) -> AuthResult<AuthResponse> {
652    match result {
653        Ok(response) => Ok(AuthResponse::json(200, &response)?),
654        Err(error)
655            if error.status_code() >= 500
656                && !matches!(error, AuthError::Upstream { .. } | AuthError::Api { .. }) =>
657        {
658            Ok(AuthResponse::new(500))
659        }
660        Err(error) => Err(error),
661    }
662}
663
664alibi_core::impl_auth_plugin! {
665    ApiKeyPlugin, "api-key";
666    routes {
667        post "/api-key/create"                    => handle_create,             "api_key_create";
668        get  "/api-key/get"                       => handle_get,                "api_key_get";
669        post "/api-key/update"                    => handle_update,             "api_key_update";
670        post "/api-key/delete"                    => handle_delete,             "api_key_delete";
671        get  "/api-key/list"                      => handle_list,               "api_key_list";
672    }
673    extra {
674    fn static_openapi_metadata(&self) -> alibi_core::PluginOpenApiMetadata {
675        crate::metadata::plugin_metadata(<Self as alibi_core::AuthPlugin<S>>::name(self), &<Self as alibi_core::AuthPlugin<S>>::routes(self))
676    }
677
678        fn openapi_metadata(&self,ctx:&alibi_core::AuthInitContext<S>)->alibi_core::PluginOpenApiMetadata {
679            let mut metadata=crate::metadata::instance_plugin_metadata("api-key",&<Self as alibi_core::AuthPlugin<S>>::routes(self),ctx);
680            let defaults=self.configurations.first().filter(|_|self.configurations.len()==1).map(|config|config.rate_limit.clone()).unwrap_or_default();
681            for model in &mut metadata.models {
682                if model.name!="Apikey" {continue;}
683                for field in &mut model.fields {
684                    let value=match field.name.as_str() {"rateLimitTimeWindow"=>defaults.time_window,"rateLimitMax"=>defaults.max_requests,_=>continue};
685                    if let Some(schema)=field.schema.as_object_mut() {_ = schema.insert("default".into(),serde_json::json!(value));}
686                }
687            }
688            metadata
689        }
690        async fn on_init(&self, _ctx: &mut alibi_core::AuthInitContext<S>) -> AuthResult<()> {
691            if self.configurations.len() > 1 {
692                let mut ids = std::collections::HashSet::new();
693                for config in &self.configurations {
694                    if config.config_id.is_empty() {
695                        return Err(AuthError::config("configId is required for each API key configuration in the api-key plugin."));
696                    }
697                    if !ids.insert(&config.config_id) {
698                        return Err(AuthError::config("configId must be unique for each API key configuration in the api-key plugin."));
699                    }
700                }
701            }
702            Ok(())
703        }
704
705        fn server_endpoints(&self) -> Vec<alibi_core::endpoint::EndpointDefinition> { endpoint::definitions() }
706
707    fn endpoint_hooks(&self) -> Vec<&dyn alibi_core::endpoint::EndpointHook<S>> { vec![self] }
708
709    fn validate_endpoint(&self, call: &alibi_core::endpoint::EndpointCall, _ctx: &AuthContext<S>) -> AuthResult<alibi_core::endpoint::EndpointInput> { endpoint::validate(call) }
710
711    async fn on_endpoint(&self, call: &alibi_core::endpoint::EndpointCall, ctx: &AuthContext<S>) -> AuthResult<alibi_core::endpoint::EndpointResponse> { self.call_endpoint(call, ctx).await }
712
713    async fn before_request(
714            &self,
715            req: &AuthRequest,
716            ctx: &AuthContext<S>,
717        ) -> AuthResult<Option<BeforeRequestAction>> {
718            self.api_key_session(req, ctx).await
719        }
720    }
721}
722
723impl std::fmt::Debug for ApiKeyPlugin {
724    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725        f.debug_struct("ApiKeyPlugin").finish_non_exhaustive()
726    }
727}
728
729pub(super) const fn api_key_error(code: ApiKeyErrorCode) -> AuthError {
730    let status = match code {
731        ApiKeyErrorCode::UnauthorizedSession => 401,
732        ApiKeyErrorCode::OrganizationPluginRequired => 500,
733        ApiKeyErrorCode::UserNotMemberOfOrganization
734        | ApiKeyErrorCode::InsufficientApiKeyPermissions => 403,
735        ApiKeyErrorCode::InvalidApiKey
736        | ApiKeyErrorCode::KeyDisabled
737        | ApiKeyErrorCode::KeyExpired
738        | ApiKeyErrorCode::UsageExceeded
739        | ApiKeyErrorCode::KeyNotFound
740        | ApiKeyErrorCode::RateLimited
741        | ApiKeyErrorCode::InvalidPrefixLength
742        | ApiKeyErrorCode::InvalidNameLength
743        | ApiKeyErrorCode::MetadataDisabled
744        | ApiKeyErrorCode::NoValuesToUpdate
745        | ApiKeyErrorCode::KeyDisabledExpiration
746        | ApiKeyErrorCode::ExpiresInTooSmall
747        | ApiKeyErrorCode::ExpiresInTooLarge
748        | ApiKeyErrorCode::InvalidRemaining
749        | ApiKeyErrorCode::RefillAmountAndIntervalRequired
750        | ApiKeyErrorCode::RefillIntervalAndAmountRequired
751        | ApiKeyErrorCode::NameRequired
752        | ApiKeyErrorCode::InvalidUserIdFromApiKey
753        | ApiKeyErrorCode::InvalidReferenceIdFromApiKey
754        | ApiKeyErrorCode::NoDefaultConfiguration
755        | ApiKeyErrorCode::OrganizationIdRequired
756        | ApiKeyErrorCode::ServerOnlyProperty
757        | ApiKeyErrorCode::FailedToUpdateApiKey
758        | ApiKeyErrorCode::InvalidMetadataType => 400,
759    };
760    AuthError::Upstream {
761        status,
762        code: code.as_str(),
763        message: code.message(),
764    }
765}
766
767fn admit_expired_cleanup(bypass: bool) -> bool {
768    let mut last = LAST_EXPIRED_CHECK
769        .lock()
770        .unwrap_or_else(std::sync::PoisonError::into_inner);
771    let now = chrono::Utc::now().timestamp_millis();
772    if !bypass && last.is_some_and(|previous| now.saturating_sub(previous) < 10_000) {
773        return false;
774    }
775    *last = Some(now);
776    true
777}
778
779/// Keys written before `config_id` existed carry no value, so absent and
780/// `"default"` denote the same configuration.
781pub(super) fn is_default_config_id(config_id: &str) -> bool {
782    config_id.is_empty() || config_id == "default"
783}
784
785/// Whether a stored key belongs to the addressed configuration.
786pub(super) fn config_id_matches(key_config_id: &str, expected: &str) -> bool {
787    if is_default_config_id(key_config_id) && is_default_config_id(expected) {
788        return true;
789    }
790    key_config_id == expected
791}
792
793// LCOV_EXCL_START
794#[cfg(test)]
795mod tests {
796
797    use super::*;
798    use alibi_core::wire::{SessionView, UserView};
799    use alibi_core::{
800        AuthContext, AuthPlugin, CreateSession, CreateUser, HttpMethod, UpdateApiKey,
801    };
802    use chrono::{Duration, Utc};
803    use std::collections::HashMap;
804    use std::sync::Arc;
805
806    type TestSchema = alibi_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
807
808    async fn create_test_context_with_user() -> (AuthContext<TestSchema>, UserView, SessionView) {
809        let config = Arc::new(alibi_core::AuthConfig::new(
810            "test-secret-key-at-least-32-chars-long",
811        ));
812        let database = crate::test_helpers::create_test_database().await;
813        let ctx = AuthContext::new(config, Arc::clone(&database));
814
815        let user = database
816            .create_user(
817                CreateUser::new()
818                    .with_email("test@example.com")
819                    .with_name("Test User"),
820            )
821            .await
822            .unwrap();
823        let wire_user = UserView::from(&user);
824
825        let session = database
826            .create_session(CreateSession {
827                additional_fields: alibi_core::field_policy::FieldValues::default(),
828                token: None,
829                active_team_id: None,
830                user_id: user.id().to_string(),
831                expires_at: Utc::now() + Duration::hours(24),
832                ip_address: Some("127.0.0.1".to_owned()),
833                user_agent: Some("test-agent".to_owned()),
834                impersonated_by: None,
835                active_organization_id: None,
836            })
837            .await
838            .unwrap();
839        let wire_session = SessionView::from(&session);
840
841        (ctx, wire_user, wire_session)
842    }
843
844    async fn create_user_with_session(
845        ctx: &AuthContext<impl alibi_core::AuthSchema>,
846        email: &str,
847    ) -> (UserView, SessionView) {
848        let user = ctx
849            .database
850            .create_user(
851                CreateUser::new()
852                    .with_email(email.to_owned())
853                    .with_name("Another User"),
854            )
855            .await
856            .unwrap();
857        let wire_user = UserView::from(&user);
858
859        let session = ctx
860            .database
861            .create_session(CreateSession {
862                additional_fields: alibi_core::field_policy::FieldValues::default(),
863                token: None,
864                active_team_id: None,
865                user_id: user.id().to_string(),
866                expires_at: Utc::now() + Duration::hours(24),
867                ip_address: None,
868                user_agent: None,
869                impersonated_by: None,
870                active_organization_id: None,
871            })
872            .await
873            .unwrap();
874        let wire_session = SessionView::from(&session);
875
876        (wire_user, wire_session)
877    }
878
879    fn create_auth_request(
880        method: HttpMethod,
881        path: &str,
882        token: Option<&str>,
883        body: Option<serde_json::Value>,
884        query: Option<HashMap<String, String>>,
885    ) -> AuthRequest {
886        let mut headers = HashMap::new();
887        if let Some(token) = token {
888            headers.insert(
889                "cookie".to_owned(),
890                format!(
891                    "better-auth.session_token={}",
892                    alibi_core::utils::cookie_utils::sign_cookie_value(
893                        token,
894                        &crate::test_helpers::create_test_config().secret
895                    )
896                ),
897            );
898        }
899
900        AuthRequest::from_parts(
901            method,
902            path.to_owned(),
903            headers,
904            body.map(|b| serde_json::to_vec(&b).unwrap()),
905            query.unwrap_or_default(),
906        )
907    }
908
909    fn json_body(response: &AuthResponse) -> serde_json::Value {
910        serde_json::from_slice(&response.body).unwrap()
911    }
912
913    /// Test helper: verify a key and return the same JSON shape the old HTTP
914    /// handler produced. Calls the public server-only verification method.
915    async fn verify_key(
916        plugin: &ApiKeyPlugin,
917        ctx: &AuthContext<impl alibi_core::AuthSchema>,
918        raw_key: &str,
919        permissions: Option<&serde_json::Value>,
920    ) -> serde_json::Value {
921        match plugin
922            .verify_api_key(
923                &VerifyApiKey {
924                    key: raw_key,
925                    config_id: None,
926                    permissions,
927                },
928                ctx,
929            )
930            .await
931        {
932            Ok(view) => serde_json::json!({ "valid": true, "error": null, "key": view }),
933            Err(ApiKeyVerificationError::Validation(error)) => serde_json::json!({
934                "valid": false,
935                "error": error,
936                "key": null,
937            }),
938            Err(
939                ApiKeyVerificationError::Internal(error)
940                | ApiKeyVerificationError::ExplicitValidator(error),
941            ) => panic!("Verification failed: {error}"),
942        }
943    }
944
945    /// Create a key via the HTTP handler (client-allowed fields only), then
946    /// patch server-only fields directly through the database.
947    ///
948    /// `server_fields` may contain: remaining, `refill_interval`, `refill_amount`,
949    /// `rate_limit_enabled`, `rate_limit_time_window`, `rate_limit_max`, permissions.
950    async fn create_key_with_server_fields(
951        plugin: &ApiKeyPlugin,
952        ctx: &AuthContext<impl alibi_core::AuthSchema>,
953        token: &str,
954        client_body: serde_json::Value,
955        server_fields: UpdateApiKey,
956    ) -> (String, String) {
957        let (id, raw_key) = create_key_and_get_raw(plugin, ctx, token, client_body).await;
958        ctx.database
959            .update_api_key(&id, server_fields)
960            .await
961            .unwrap();
962        (id, raw_key)
963    }
964
965    /// Test helper: delete all expired keys and return a success JSON.
966    async fn delete_all_expired(
967        ctx: &AuthContext<impl alibi_core::AuthSchema>,
968    ) -> serde_json::Value {
969        _ = ctx.database.delete_expired_api_keys().await.unwrap();
970        serde_json::json!({ "success": true, "error": null })
971    }
972
973    async fn create_key_and_get_id(
974        plugin: &ApiKeyPlugin,
975        ctx: &AuthContext<impl alibi_core::AuthSchema>,
976        token: &str,
977        name: &str,
978    ) -> String {
979        let req = create_auth_request(
980            HttpMethod::Post,
981            "/api-key/create",
982            Some(token),
983            Some(serde_json::json!({ "name": name })),
984            None,
985        );
986        let response = plugin.handle_create(&req, ctx).await.unwrap();
987        assert_eq!(response.status, 200);
988        (*(json_body(&response))
989            .get("id")
990            .unwrap_or(&serde_json::Value::Null))
991        .as_str()
992        .unwrap()
993        .to_owned()
994    }
995
996    /// Helper: create a key and return (id, `raw_key`)
997    async fn create_key_and_get_raw(
998        plugin: &ApiKeyPlugin,
999        ctx: &AuthContext<impl alibi_core::AuthSchema>,
1000        token: &str,
1001        body: serde_json::Value,
1002    ) -> (String, String) {
1003        let req = create_auth_request(
1004            HttpMethod::Post,
1005            "/api-key/create",
1006            Some(token),
1007            Some(body),
1008            None,
1009        );
1010        let response = plugin.handle_create(&req, ctx).await.unwrap();
1011        assert_eq!(response.status, 200);
1012        let b = json_body(&response);
1013        (
1014            (*(b).get("id").expect("fixture contains the requested index"))
1015                .as_str()
1016                .unwrap()
1017                .to_owned(),
1018            (*(b)
1019                .get("key")
1020                .expect("fixture contains the requested index"))
1021            .as_str()
1022            .unwrap()
1023            .to_owned(),
1024        )
1025    }
1026
1027    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1028    #[tokio::test]
1029    async fn test_create_and_get_do_not_expose_hash() {
1030        let plugin = ApiKeyPlugin::builder().prefix("ba_".to_owned()).build();
1031        let (ctx, _user, session) = create_test_context_with_user().await;
1032
1033        let create_req = create_auth_request(
1034            HttpMethod::Post,
1035            "/api-key/create",
1036            Some(&session.token),
1037            Some(serde_json::json!({ "name": "primary" })),
1038            None,
1039        );
1040        let create_response = plugin.handle_create(&create_req, &ctx).await.unwrap();
1041        assert_eq!(create_response.status, 200);
1042
1043        let body = json_body(&create_response);
1044        assert!(body.get("key").is_some());
1045        assert!(body.get("key_hash").is_none());
1046        assert!(body.get("hash").is_none());
1047
1048        let id = (*(body).get("id").unwrap_or(&serde_json::Value::Null))
1049            .as_str()
1050            .unwrap();
1051        let mut query = HashMap::new();
1052        query.insert("id".to_owned(), id.to_owned());
1053
1054        let get_req = create_auth_request(
1055            HttpMethod::Get,
1056            "/api-key/get",
1057            Some(&session.token),
1058            None,
1059            Some(query),
1060        );
1061        let get_response = plugin.handle_get(&get_req, &ctx).await.unwrap();
1062        assert_eq!(get_response.status, 200);
1063
1064        let get_body = json_body(&get_response);
1065        assert!(get_body.get("key").is_none());
1066        assert!(get_body.get("key_hash").is_none());
1067    }
1068
1069    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1070    #[tokio::test]
1071    async fn test_create_rejects_invalid_expires_in() {
1072        let plugin = ApiKeyPlugin::builder().build();
1073        let (ctx, _user, session) = create_test_context_with_user().await;
1074
1075        let req = create_auth_request(
1076            HttpMethod::Post,
1077            "/api-key/create",
1078            Some(&session.token),
1079            Some(serde_json::json!({ "expiresIn": -1 })),
1080            None,
1081        );
1082        let response = plugin.handle_create(&req, &ctx).await;
1083        // Should be rejected due to validation (negative expires_in)
1084        assert!(response.is_err() || response.unwrap().status != 200);
1085    }
1086
1087    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1088    #[tokio::test]
1089    async fn test_get_update_delete_return_404_for_non_owner() {
1090        let plugin = ApiKeyPlugin::builder().build();
1091        let (ctx, _user1, session1) = create_test_context_with_user().await;
1092        let (_user2, session2) = create_user_with_session(&ctx, "other@example.com").await;
1093        let key_id = create_key_and_get_id(&plugin, &ctx, &session1.token, "owner-key").await;
1094
1095        let mut get_query = HashMap::new();
1096        get_query.insert("id".to_owned(), key_id.clone());
1097        let get_req = create_auth_request(
1098            HttpMethod::Get,
1099            "/api-key/get",
1100            Some(&session2.token),
1101            None,
1102            Some(get_query),
1103        );
1104        let get_err = plugin.handle_get(&get_req, &ctx).await.unwrap_err();
1105        assert_eq!(get_err.status_code(), 404);
1106
1107        let update_req = create_auth_request(
1108            HttpMethod::Post,
1109            "/api-key/update",
1110            Some(&session2.token),
1111            Some(serde_json::json!({ "keyId": key_id, "name": "new-name" })),
1112            None,
1113        );
1114        let update_err = plugin.handle_update(&update_req, &ctx).await.unwrap_err();
1115        assert_eq!(update_err.status_code(), 404);
1116
1117        let delete_req = create_auth_request(
1118            HttpMethod::Post,
1119            "/api-key/delete",
1120            Some(&session2.token),
1121            Some(serde_json::json!({ "keyId": key_id })),
1122            None,
1123        );
1124        let delete_err = plugin.handle_delete(&delete_req, &ctx).await.unwrap_err();
1125        assert_eq!(delete_err.status_code(), 404);
1126    }
1127
1128    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1129    #[tokio::test]
1130    async fn test_list_returns_only_user_keys() {
1131        let plugin = ApiKeyPlugin::builder().build();
1132        let (ctx, user1, session1) = create_test_context_with_user().await;
1133        let (_user2, session2) = create_user_with_session(&ctx, "other@example.com").await;
1134
1135        _ = create_key_and_get_id(&plugin, &ctx, &session1.token, "u1-key").await;
1136        _ = create_key_and_get_id(&plugin, &ctx, &session2.token, "u2-key").await;
1137
1138        let list_req = create_auth_request(
1139            HttpMethod::Get,
1140            "/api-key/list",
1141            Some(&session1.token),
1142            None,
1143            None,
1144        );
1145        let list_response = plugin.handle_list(&list_req, &ctx).await.unwrap();
1146        assert_eq!(list_response.status, 200);
1147
1148        let list_body = json_body(&list_response);
1149        // Upstream returns a paginated envelope rather than a bare array.
1150        let list = (*(list_body)
1151            .get("apiKeys")
1152            .unwrap_or(&serde_json::Value::Null))
1153        .as_array()
1154        .unwrap();
1155        assert_eq!(list.len(), 1);
1156        assert_eq!(
1157            (*(list_body).get("total").unwrap_or(&serde_json::Value::Null)),
1158            1
1159        );
1160        // Upstream renamed the owner field to `referenceId` (a user or an
1161        // organization id) and stamps the owning configuration on every key.
1162        assert_eq!(
1163            (*(*(list)
1164                .first()
1165                .expect("fixture contains the requested index"))
1166            .get("referenceId")
1167            .expect("fixture contains the requested index"))
1168            .as_str()
1169            .unwrap(),
1170            user1.id
1171        );
1172        assert_eq!(
1173            (*(*(list)
1174                .first()
1175                .expect("fixture contains the requested index"))
1176            .get("configId")
1177            .expect("fixture contains the requested index"))
1178            .as_str()
1179            .unwrap(),
1180            "default"
1181        );
1182        assert!(
1183            (*(list)
1184                .first()
1185                .expect("fixture contains the requested index"))
1186            .get("userId")
1187            .is_none()
1188        );
1189        assert!(
1190            (*(list)
1191                .first()
1192                .expect("fixture contains the requested index"))
1193            .get("key")
1194            .is_none()
1195        );
1196        assert!(
1197            (*(list)
1198                .first()
1199                .expect("fixture contains the requested index"))
1200            .get("key_hash")
1201            .is_none()
1202        );
1203    }
1204
1205    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1206    #[tokio::test]
1207    async fn test_owner_can_delete_key() {
1208        let plugin = ApiKeyPlugin::builder().build();
1209        let (ctx, _user, session) = create_test_context_with_user().await;
1210        let key_id = create_key_and_get_id(&plugin, &ctx, &session.token, "to-delete").await;
1211
1212        let delete_req = create_auth_request(
1213            HttpMethod::Post,
1214            "/api-key/delete",
1215            Some(&session.token),
1216            Some(serde_json::json!({ "keyId": key_id })),
1217            None,
1218        );
1219        let delete_response = plugin.handle_delete(&delete_req, &ctx).await.unwrap();
1220        assert_eq!(delete_response.status, 200);
1221
1222        let deleted = ctx.database.get_api_key_by_id(&key_id).await.unwrap();
1223        assert!(deleted.is_none());
1224    }
1225
1226    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1227    #[tokio::test]
1228    async fn test_verify_valid_key() {
1229        let plugin = ApiKeyPlugin::builder().build();
1230        let (ctx, _user, session) = create_test_context_with_user().await;
1231
1232        let (_id, raw_key) = create_key_and_get_raw(
1233            &plugin,
1234            &ctx,
1235            &session.token,
1236            serde_json::json!({ "name": "verify-test" }),
1237        )
1238        .await;
1239
1240        let body = verify_key(&plugin, &ctx, &raw_key, None).await;
1241        assert_eq!(
1242            (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1243            true
1244        );
1245        assert!((*(body).get("key").unwrap_or(&serde_json::Value::Null)).is_object());
1246    }
1247
1248    #[tokio::test]
1249    async fn test_verify_invalid_key() {
1250        let plugin = ApiKeyPlugin::builder().build();
1251        let (ctx, _user, _session) = create_test_context_with_user().await;
1252
1253        let body = verify_key(&plugin, &ctx, "definitely-not-a-valid-key", None).await;
1254        assert_eq!(
1255            (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1256            false
1257        );
1258        assert!((*(body).get("error").unwrap_or(&serde_json::Value::Null)).is_object());
1259    }
1260
1261    #[tokio::test]
1262    async fn test_verify_disabled_key() {
1263        let plugin = ApiKeyPlugin::builder().build();
1264        let (ctx, _user, session) = create_test_context_with_user().await;
1265
1266        let (id, raw_key) = create_key_and_get_raw(
1267            &plugin,
1268            &ctx,
1269            &session.token,
1270            serde_json::json!({ "name": "disable-test" }),
1271        )
1272        .await;
1273
1274        let update = UpdateApiKey {
1275            enabled: Some(false),
1276            ..Default::default()
1277        };
1278        ctx.database.update_api_key(&id, update).await.unwrap();
1279
1280        let body = verify_key(&plugin, &ctx, &raw_key, None).await;
1281        assert_eq!(
1282            (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1283            false
1284        );
1285        assert_eq!(
1286            (*(*(body).get("error").unwrap_or(&serde_json::Value::Null))
1287                .get("code")
1288                .unwrap_or(&serde_json::Value::Null)),
1289            "KEY_DISABLED"
1290        );
1291    }
1292
1293    #[tokio::test]
1294    async fn test_verify_expired_key() {
1295        let plugin = ApiKeyPlugin::builder().build();
1296        let (ctx, _user, session) = create_test_context_with_user().await;
1297
1298        let (id, raw_key) = create_key_and_get_raw(
1299            &plugin,
1300            &ctx,
1301            &session.token,
1302            serde_json::json!({ "name": "expire-test" }),
1303        )
1304        .await;
1305
1306        let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
1307        let update = UpdateApiKey {
1308            expires_at: Some(Some(past)),
1309            ..Default::default()
1310        };
1311        ctx.database.update_api_key(&id, update).await.unwrap();
1312
1313        let body = verify_key(&plugin, &ctx, &raw_key, None).await;
1314        assert_eq!(
1315            (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1316            false
1317        );
1318        assert_eq!(
1319            (*(*(body).get("error").unwrap_or(&serde_json::Value::Null))
1320                .get("code")
1321                .unwrap_or(&serde_json::Value::Null)),
1322            "KEY_EXPIRED"
1323        );
1324
1325        // The key should have been deleted
1326        let deleted = ctx.database.get_api_key_by_id(&id).await.unwrap();
1327        assert!(deleted.is_none());
1328    }
1329
1330    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1331    #[tokio::test]
1332    async fn test_verify_remaining_consumption() {
1333        let plugin = ApiKeyPlugin::builder().build();
1334        let (ctx, _user, session) = create_test_context_with_user().await;
1335
1336        let (_id, raw_key) = create_key_with_server_fields(
1337            &plugin,
1338            &ctx,
1339            &session.token,
1340            serde_json::json!({ "name": "remain-test" }),
1341            UpdateApiKey {
1342                remaining: Some(2.0),
1343                ..Default::default()
1344            },
1345        )
1346        .await;
1347
1348        // First verify - remaining goes from 2 to 1
1349        let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1350        assert_eq!(
1351            (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1352            true
1353        );
1354        assert_eq!(
1355            (*(*(r1).get("key").unwrap_or(&serde_json::Value::Null))
1356                .get("remaining")
1357                .unwrap_or(&serde_json::Value::Null)),
1358            1
1359        );
1360
1361        // Second verify - remaining goes from 1 to 0
1362        let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1363        assert_eq!(
1364            (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1365            true
1366        );
1367        assert_eq!(
1368            (*(*(r2).get("key").unwrap_or(&serde_json::Value::Null))
1369                .get("remaining")
1370                .unwrap_or(&serde_json::Value::Null)),
1371            0
1372        );
1373
1374        // Third verify - should fail (usage exceeded)
1375        let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1376        assert_eq!(
1377            (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1378            false
1379        );
1380        assert_eq!(
1381            (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1382                .get("code")
1383                .unwrap_or(&serde_json::Value::Null)),
1384            "USAGE_EXCEEDED"
1385        );
1386    }
1387
1388    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1389    #[tokio::test]
1390    async fn test_verify_rate_limiting() {
1391        let plugin = ApiKeyPlugin::builder()
1392            .rate_limit(RateLimitDefaults {
1393                enabled: true,
1394                time_window: 60_000.0,
1395                max_requests: 2.0,
1396            })
1397            .build();
1398        let (ctx, _user, session) = create_test_context_with_user().await;
1399
1400        let (_id, raw_key) = create_key_with_server_fields(
1401            &plugin,
1402            &ctx,
1403            &session.token,
1404            serde_json::json!({ "name": "rl-test" }),
1405            UpdateApiKey {
1406                rate_limit_enabled: Some(true),
1407                rate_limit_time_window: Some(60_000.0),
1408                rate_limit_max: Some(2.0),
1409                ..Default::default()
1410            },
1411        )
1412        .await;
1413
1414        // First two should succeed
1415        let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1416        assert_eq!(
1417            (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1418            true
1419        );
1420
1421        let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1422        assert_eq!(
1423            (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1424            true
1425        );
1426
1427        // Third should fail with rate limit
1428        let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1429        assert_eq!(
1430            (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1431            false
1432        );
1433        assert_eq!(
1434            (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1435                .get("code")
1436                .unwrap_or(&serde_json::Value::Null)),
1437            "RATE_LIMITED"
1438        );
1439    }
1440
1441    #[tokio::test]
1442    async fn test_delete_all_expired() {
1443        let plugin = ApiKeyPlugin::builder().build();
1444        let (ctx, fixture_user, session) = create_test_context_with_user().await;
1445
1446        // Create two keys
1447        let (id1, _) = create_key_and_get_raw(
1448            &plugin,
1449            &ctx,
1450            &session.token,
1451            serde_json::json!({ "name": "will-expire" }),
1452        )
1453        .await;
1454        let (_id2, _) = create_key_and_get_raw(
1455            &plugin,
1456            &ctx,
1457            &session.token,
1458            serde_json::json!({ "name": "wont-expire" }),
1459        )
1460        .await;
1461
1462        // Expire the first key
1463        let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
1464        ctx.database
1465            .update_api_key(
1466                &id1,
1467                UpdateApiKey {
1468                    expires_at: Some(Some(past)),
1469                    ..Default::default()
1470                },
1471            )
1472            .await
1473            .unwrap();
1474
1475        let body = delete_all_expired(&ctx).await;
1476        assert_eq!(
1477            (*(body).get("success").unwrap_or(&serde_json::Value::Null)),
1478            true
1479        );
1480
1481        // Only the non-expired key should remain
1482        let remaining_keys = ctx
1483            .database
1484            .list_api_keys_by_reference(&fixture_user.id)
1485            .await
1486            .unwrap();
1487        assert_eq!(remaining_keys.len(), 1);
1488    }
1489
1490    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1491    #[tokio::test]
1492    async fn test_verify_permissions() {
1493        let plugin = ApiKeyPlugin::builder().build();
1494        let (ctx, _user, session) = create_test_context_with_user().await;
1495
1496        let (_id, raw_key) = create_key_with_server_fields(
1497            &plugin,
1498            &ctx,
1499            &session.token,
1500            serde_json::json!({ "name": "perm-test" }),
1501            UpdateApiKey {
1502                permissions: Some(
1503                    serde_json::to_string(&serde_json::json!({
1504                        "admin": ["read", "write"],
1505                        "user": ["read"]
1506                    }))
1507                    .unwrap(),
1508                ),
1509                ..Default::default()
1510            },
1511        )
1512        .await;
1513
1514        // Verify with matching permissions -> should pass
1515        let perms_ok = serde_json::json!({ "admin": ["read"] });
1516        let r1 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_ok)).await;
1517        assert_eq!(
1518            (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1519            true
1520        );
1521
1522        // Verify with non-matching permissions -> should fail
1523        let perms_fail = serde_json::json!({ "superadmin": ["delete"] });
1524        let r2 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_fail)).await;
1525        assert_eq!(
1526            (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1527            false
1528        );
1529    }
1530
1531    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1532    #[tokio::test]
1533    async fn test_config_validation_prefix_length() {
1534        let plugin = ApiKeyPlugin::builder()
1535            .min_prefix_length(2)
1536            .max_prefix_length(5)
1537            .build();
1538        let (ctx, _user, session) = create_test_context_with_user().await;
1539
1540        // Too short prefix
1541        let req = create_auth_request(
1542            HttpMethod::Post,
1543            "/api-key/create",
1544            Some(&session.token),
1545            Some(serde_json::json!({ "name": "test", "prefix": "a" })),
1546            None,
1547        );
1548        let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1549        assert!(err.to_string().contains("prefix length"));
1550
1551        // Too long prefix
1552        let req2 = create_auth_request(
1553            HttpMethod::Post,
1554            "/api-key/create",
1555            Some(&session.token),
1556            Some(serde_json::json!({ "name": "test", "prefix": "toolong" })),
1557            None,
1558        );
1559        let err2 = plugin.handle_create(&req2, &ctx).await.unwrap_err();
1560        assert!(err2.to_string().contains("prefix length"));
1561    }
1562
1563    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1564    #[tokio::test]
1565    async fn test_config_require_name() {
1566        let plugin = ApiKeyPlugin::builder().require_name(true).build();
1567        let (ctx, _user, session) = create_test_context_with_user().await;
1568
1569        // No name provided -> should fail
1570        let req = create_auth_request(
1571            HttpMethod::Post,
1572            "/api-key/create",
1573            Some(&session.token),
1574            Some(serde_json::json!({})),
1575            None,
1576        );
1577        let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1578        assert!(err.to_string().contains("name is required"));
1579    }
1580
1581    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1582    #[tokio::test]
1583    async fn test_config_metadata_disabled() {
1584        let plugin = ApiKeyPlugin::builder().build(); // enable_metadata defaults to false
1585        let (ctx, _user, session) = create_test_context_with_user().await;
1586
1587        let req = create_auth_request(
1588            HttpMethod::Post,
1589            "/api-key/create",
1590            Some(&session.token),
1591            Some(serde_json::json!({ "name": "test", "metadata": { "env": "prod" } })),
1592            None,
1593        );
1594        let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1595        assert!(err.to_string().contains("Metadata is disabled"));
1596    }
1597
1598    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1599    #[tokio::test]
1600    async fn test_config_metadata_enabled() {
1601        let plugin = ApiKeyPlugin::builder().enable_metadata(true).build();
1602        let (ctx, _user, session) = create_test_context_with_user().await;
1603
1604        let req = create_auth_request(
1605            HttpMethod::Post,
1606            "/api-key/create",
1607            Some(&session.token),
1608            Some(serde_json::json!({ "name": "test", "metadata": { "env": "prod" } })),
1609            None,
1610        );
1611        let resp = plugin.handle_create(&req, &ctx).await.unwrap();
1612        assert_eq!(resp.status, 200);
1613        let body = json_body(&resp);
1614        assert_eq!(
1615            (*(*(body).get("metadata").unwrap_or(&serde_json::Value::Null))
1616                .get("env")
1617                .unwrap_or(&serde_json::Value::Null)),
1618            "prod"
1619        );
1620    }
1621
1622    // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1623    #[tokio::test]
1624    async fn test_update_with_expires_in() {
1625        let plugin = ApiKeyPlugin::builder().build();
1626        let (ctx, _user, session) = create_test_context_with_user().await;
1627        let key_id = create_key_and_get_id(&plugin, &ctx, &session.token, "update-exp").await;
1628
1629        let update_req = create_auth_request(
1630            HttpMethod::Post,
1631            "/api-key/update",
1632            Some(&session.token),
1633            Some(serde_json::json!({
1634                "keyId": key_id,
1635                "expiresIn": 86400
1636            })),
1637            None,
1638        );
1639        let resp = plugin.handle_update(&update_req, &ctx).await.unwrap();
1640        assert_eq!(resp.status, 200);
1641        let body = json_body(&resp);
1642        assert!((*(body).get("expiresAt").unwrap_or(&serde_json::Value::Null)).is_string());
1643    }
1644
1645    // Note: /api-key/verify and /api-key/delete-all-expired-api-keys are
1646    // server-only in TS (no HTTP routes), so the Rust plugin does not expose
1647    // them either. Route dispatch tests for these have been removed.
1648
1649    // Ensure refillInterval + refillAmount require each other (validation logic)
1650    #[tokio::test]
1651    async fn test_refill_logic() {
1652        let result = ApiKeyPlugin::validate_refill(Some(60_000.0), None);
1653        assert!(result.is_err());
1654        assert!(result.unwrap_err().to_string().contains("refillAmount"));
1655
1656        let result_2 = ApiKeyPlugin::validate_refill(None, Some(10.0));
1657        assert!(result_2.is_err());
1658
1659        let result_3 = ApiKeyPlugin::validate_refill(Some(60_000.0), Some(10.0));
1660        assert!(result_3.is_ok());
1661    }
1662
1663    mod session_tests {
1664        use super::*;
1665
1666        #[tokio::test]
1667        async fn test_virtual_session_answers_get_and_post_get_session() {
1668            use alibi_core::AuthPlugin;
1669
1670            let plugin = ApiKeyPlugin::builder()
1671                .enable_session_for_api_keys(true)
1672                .build();
1673            let (ctx, user, session) = create_test_context_with_user().await;
1674            let (id, raw_key) = create_key_and_get_raw(
1675                &plugin,
1676                &ctx,
1677                &session.token,
1678                serde_json::json!({ "name": "get-session" }),
1679            )
1680            .await;
1681
1682            for method in [HttpMethod::Get, HttpMethod::Post] {
1683                let mut request = create_auth_request(method, "/get-session", None, None, None);
1684                _ = request
1685                    .headers
1686                    .insert("x-api-key".to_owned(), raw_key.clone());
1687                let action = AuthPlugin::<TestSchema>::before_request(&plugin, &request, &ctx)
1688                    .await
1689                    .unwrap();
1690                let Some(BeforeRequestAction::Respond(response)) = action else {
1691                    panic!("API key sessions must answer GET and POST before route dispatch")
1692                };
1693                assert_eq!(response.status, 200);
1694                let body = json_body(&response);
1695                assert_eq!(
1696                    (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1697                        .get("id")
1698                        .unwrap_or(&serde_json::Value::Null)),
1699                    id
1700                );
1701                assert_eq!(
1702                    (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1703                        .get("token")
1704                        .unwrap_or(&serde_json::Value::Null)),
1705                    raw_key
1706                );
1707                assert_eq!(
1708                    (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1709                        .get("userId")
1710                        .unwrap_or(&serde_json::Value::Null)),
1711                    user.id
1712                );
1713            }
1714        }
1715
1716        // 1. Virtual session: before_request injects session without DB writes
1717        // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1718        #[tokio::test]
1719        async fn test_virtual_session_creates_no_db_session() {
1720            let plugin = ApiKeyPlugin::builder()
1721                .enable_session_for_api_keys(true)
1722                .build();
1723            let (ctx, fixture_user, session) = create_test_context_with_user().await;
1724
1725            // Create an API key
1726            let (_id, raw_key) = create_key_and_get_raw(
1727                &plugin,
1728                &ctx,
1729                &session.token,
1730                serde_json::json!({ "name": "virtual-session-test" }),
1731            )
1732            .await;
1733
1734            // Count sessions before
1735            let sessions_before = ctx
1736                .database
1737                .get_user_sessions(&fixture_user.id)
1738                .await
1739                .unwrap()
1740                .len();
1741
1742            // Simulate a request to a protected route with only x-api-key header
1743            let mut headers = HashMap::new();
1744            headers.insert("x-api-key".to_owned(), raw_key.clone());
1745            let req = AuthRequest::from_parts(
1746                HttpMethod::Post,
1747                "/update-user".to_owned(),
1748                headers,
1749                None,
1750                HashMap::new(),
1751            );
1752
1753            // Call before_request -- should return InjectSession
1754            let action = plugin.before_request(&req, &ctx).await.unwrap();
1755            assert!(action.is_some(), "before_request should return an action");
1756            match action.unwrap() {
1757                BeforeRequestAction::InjectSession { session: session_2 } => {
1758                    assert_eq!(session_2.user_id, fixture_user.id);
1759                }
1760                BeforeRequestAction::Respond(_) | BeforeRequestAction::ReplaceHeaders { .. } => {
1761                    panic!("Expected InjectSession, got Respond");
1762                }
1763            }
1764
1765            // Count sessions after -- should be unchanged (no DB writes)
1766            let sessions_after = ctx
1767                .database
1768                .get_user_sessions(&fixture_user.id)
1769                .await
1770                .unwrap()
1771                .len();
1772            assert_eq!(
1773                sessions_before, sessions_after,
1774                "No new sessions should be created in the database"
1775            );
1776        }
1777
1778        // 2. Virtual session on /get-session: synthetic response
1779        // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1780        #[tokio::test]
1781        async fn test_virtual_session_on_get_session() {
1782            let plugin = ApiKeyPlugin::builder()
1783                .enable_session_for_api_keys(true)
1784                .build();
1785            let (ctx, user, session) = create_test_context_with_user().await;
1786
1787            let (_id, raw_key) = create_key_and_get_raw(
1788                &plugin,
1789                &ctx,
1790                &session.token,
1791                serde_json::json!({ "name": "get-session-test" }),
1792            )
1793            .await;
1794
1795            // Send request to /get-session with x-api-key header
1796            let mut headers = HashMap::new();
1797            headers.insert("x-api-key".to_owned(), raw_key.clone());
1798            let req = AuthRequest::from_parts(
1799                HttpMethod::Get,
1800                "/get-session".to_owned(),
1801                headers,
1802                None,
1803                HashMap::new(),
1804            );
1805
1806            let action = plugin.before_request(&req, &ctx).await.unwrap();
1807            assert!(action.is_some());
1808            match action.unwrap() {
1809                BeforeRequestAction::Respond(resp) => {
1810                    assert_eq!(resp.status, 200);
1811                    let body: serde_json::Value = serde_json::from_slice(&resp.body).unwrap();
1812                    // Should contain user data
1813                    assert_eq!(
1814                        (*(*(body).get("user").unwrap_or(&serde_json::Value::Null))
1815                            .get("id")
1816                            .unwrap_or(&serde_json::Value::Null)),
1817                        user.id
1818                    );
1819                    assert_eq!(
1820                        (*(*(body).get("user").unwrap_or(&serde_json::Value::Null))
1821                            .get("email")
1822                            .unwrap_or(&serde_json::Value::Null)),
1823                        "test@example.com"
1824                    );
1825                    // Should contain session-like data
1826                    assert!(
1827                        (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1828                            .get("id")
1829                            .unwrap_or(&serde_json::Value::Null))
1830                        .is_string()
1831                    );
1832                    assert_eq!(
1833                        (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1834                            .get("userId")
1835                            .unwrap_or(&serde_json::Value::Null)),
1836                        user.id
1837                    );
1838                }
1839                BeforeRequestAction::InjectSession { .. }
1840                | BeforeRequestAction::ReplaceHeaders { .. } => {
1841                    panic!("Expected Respond for /get-session, got InjectSession");
1842                }
1843            }
1844        }
1845
1846        // 3. Rate limiting: create key with rateLimitMax=2, 3rd call fails
1847        // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1848        #[tokio::test]
1849        async fn test_rate_limiting_third_call_fails() {
1850            let plugin = ApiKeyPlugin::builder()
1851                .rate_limit(RateLimitDefaults {
1852                    enabled: true,
1853                    time_window: 60_000.0,
1854                    max_requests: 2.0,
1855                })
1856                .build();
1857            let (ctx, _user, session) = create_test_context_with_user().await;
1858
1859            let (_id, raw_key) = create_key_with_server_fields(
1860                &plugin,
1861                &ctx,
1862                &session.token,
1863                serde_json::json!({ "name": "rl-integration" }),
1864                UpdateApiKey {
1865                    rate_limit_enabled: Some(true),
1866                    rate_limit_time_window: Some(60_000.0),
1867                    rate_limit_max: Some(2.0),
1868                    ..Default::default()
1869                },
1870            )
1871            .await;
1872
1873            // First two pass
1874            let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1875            assert_eq!(
1876                (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1877                true,
1878                "1st request should pass"
1879            );
1880
1881            let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1882            assert_eq!(
1883                (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1884                true,
1885                "2nd request should pass"
1886            );
1887
1888            // Third should fail
1889            let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1890            assert_eq!(
1891                (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1892                false,
1893                "3rd request should be rate-limited"
1894            );
1895            assert_eq!(
1896                (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1897                    .get("code")
1898                    .unwrap_or(&serde_json::Value::Null)),
1899                "RATE_LIMITED"
1900            );
1901        }
1902
1903        // 4. Remaining consumption: remaining=2, no refill, 3rd fails
1904        // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1905        #[tokio::test]
1906        async fn test_remaining_consumption_no_refill() {
1907            let plugin = ApiKeyPlugin::builder().build();
1908            let (ctx, _user, session) = create_test_context_with_user().await;
1909
1910            let (_id, raw_key) = create_key_with_server_fields(
1911                &plugin,
1912                &ctx,
1913                &session.token,
1914                serde_json::json!({ "name": "remaining-test" }),
1915                UpdateApiKey {
1916                    remaining: Some(2.0),
1917                    ..Default::default()
1918                },
1919            )
1920            .await;
1921
1922            // 1st: remaining 2->1
1923            let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1924            assert_eq!(
1925                (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1926                true
1927            );
1928            assert_eq!(
1929                (*(*(r1).get("key").unwrap_or(&serde_json::Value::Null))
1930                    .get("remaining")
1931                    .unwrap_or(&serde_json::Value::Null)),
1932                1
1933            );
1934
1935            // 2nd: remaining 1->0
1936            let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1937            assert_eq!(
1938                (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1939                true
1940            );
1941            assert_eq!(
1942                (*(*(r2).get("key").unwrap_or(&serde_json::Value::Null))
1943                    .get("remaining")
1944                    .unwrap_or(&serde_json::Value::Null)),
1945                0
1946            );
1947
1948            // 3rd: usage exceeded
1949            let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1950            assert_eq!(
1951                (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1952                false
1953            );
1954            assert_eq!(
1955                (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1956                    .get("code")
1957                    .unwrap_or(&serde_json::Value::Null)),
1958                "USAGE_EXCEEDED"
1959            );
1960        }
1961
1962        // 5. Refill logic: remaining=1, refillInterval=100ms, refillAmount=10,
1963        //    verify once -> remaining=0, wait 150ms, verify -> refill to 10 then
1964        //    decrement to 9.
1965        // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
1966        #[tokio::test]
1967        async fn test_refill_resets_remaining_after_interval() {
1968            let plugin = ApiKeyPlugin::builder().build();
1969            let (ctx, _user, session) = create_test_context_with_user().await;
1970
1971            // Use a very short refill interval for testing (100 ms)
1972            let (_id, raw_key) = create_key_with_server_fields(
1973                &plugin,
1974                &ctx,
1975                &session.token,
1976                serde_json::json!({ "name": "refill-test" }),
1977                UpdateApiKey {
1978                    remaining: Some(1.0),
1979                    refill_interval: Some(100.0),
1980                    refill_amount: Some(10.0),
1981                    ..Default::default()
1982                },
1983            )
1984            .await;
1985
1986            // First verify: remaining 1->0
1987            let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1988            assert_eq!(
1989                (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1990                true
1991            );
1992            assert_eq!(
1993                (*(*(r1).get("key").unwrap_or(&serde_json::Value::Null))
1994                    .get("remaining")
1995                    .unwrap_or(&serde_json::Value::Null)),
1996                0
1997            );
1998
1999            // Wait for refill interval to elapse
2000            tokio::time::sleep(std::time::Duration::from_millis(150)).await;
2001
2002            // Second verify: should refill to 10 and then decrement -> 9
2003            let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
2004            assert_eq!(
2005                (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
2006                true,
2007                "Should succeed after refill"
2008            );
2009            assert_eq!(
2010                (*(*(r2).get("key").unwrap_or(&serde_json::Value::Null))
2011                    .get("remaining")
2012                    .unwrap_or(&serde_json::Value::Null)),
2013                9,
2014                "Should be refillAmount - 1 = 9"
2015            );
2016        }
2017
2018        // 6. Permissions: key with {"admin": ["read"]}, verify with
2019        //    {"admin": ["write"]} should fail
2020        // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
2021        #[tokio::test]
2022        async fn test_permissions_mismatch_fails() {
2023            let plugin = ApiKeyPlugin::builder().build();
2024            let (ctx, _user, session) = create_test_context_with_user().await;
2025
2026            let (_id, raw_key) = create_key_with_server_fields(
2027                &plugin,
2028                &ctx,
2029                &session.token,
2030                serde_json::json!({ "name": "perm-mismatch" }),
2031                UpdateApiKey {
2032                    permissions: Some(
2033                        serde_json::to_string(&serde_json::json!({ "admin": ["read"] })).unwrap(),
2034                    ),
2035                    ..Default::default()
2036                },
2037            )
2038            .await;
2039
2040            // Verify with matching permission -> pass
2041            let perms_ok = serde_json::json!({ "admin": ["read"] });
2042            let r1 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_ok)).await;
2043            assert_eq!(
2044                (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
2045                true
2046            );
2047
2048            // Verify with mismatched permission -> fail
2049            let perms_fail = serde_json::json!({ "admin": ["write"] });
2050            let r2 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_fail)).await;
2051            assert_eq!(
2052                (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
2053                false
2054            );
2055        }
2056
2057        // 7. Concurrent rate limiting: send 5 sequential verify requests with
2058        //    rateLimitMax=2, only first 2 succeed (sequential proves logic is
2059        //    correct; true concurrency race conditions are documented above).
2060        // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
2061        #[tokio::test]
2062        async fn test_concurrent_rate_limiting() {
2063            let plugin = ApiKeyPlugin::builder()
2064                .rate_limit(RateLimitDefaults {
2065                    enabled: true,
2066                    time_window: 60_000.0,
2067                    max_requests: 2.0,
2068                })
2069                .build();
2070            let (ctx, _user, session) = create_test_context_with_user().await;
2071
2072            let (_id, raw_key) = create_key_with_server_fields(
2073                &plugin,
2074                &ctx,
2075                &session.token,
2076                serde_json::json!({ "name": "concurrent-rl" }),
2077                UpdateApiKey {
2078                    rate_limit_enabled: Some(true),
2079                    rate_limit_time_window: Some(60_000.0),
2080                    rate_limit_max: Some(2.0),
2081                    ..Default::default()
2082                },
2083            )
2084            .await;
2085
2086            let mut success_count = 0;
2087            let mut fail_count = 0;
2088
2089            for _ in 0..5 {
2090                let body = verify_key(&plugin, &ctx, &raw_key, None).await;
2091                if (*(body).get("valid").unwrap_or(&serde_json::Value::Null)) == true {
2092                    success_count += 1;
2093                } else {
2094                    fail_count += 1;
2095                    assert_eq!(
2096                        (*(*(body).get("error").unwrap_or(&serde_json::Value::Null))
2097                            .get("code")
2098                            .unwrap_or(&serde_json::Value::Null)),
2099                        "RATE_LIMITED"
2100                    );
2101                }
2102            }
2103
2104            assert_eq!(success_count, 2, "Only 2 out of 5 should succeed");
2105            assert_eq!(fail_count, 3, "3 out of 5 should be rate-limited");
2106        }
2107
2108        // 8. Database compatibility: test delete_expired_api_keys through the
2109        //    in-repo auth store implementation.
2110        // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
2111        #[tokio::test]
2112        async fn test_delete_expired_api_keys_memory_adapter() {
2113            let (ctx, fixture_user, session) = create_test_context_with_user().await;
2114            let plugin = ApiKeyPlugin::builder().build();
2115
2116            // Create two keys
2117            let (id1, _) = create_key_and_get_raw(
2118                &plugin,
2119                &ctx,
2120                &session.token,
2121                serde_json::json!({ "name": "will-expire" }),
2122            )
2123            .await;
2124            let (_id2, _) = create_key_and_get_raw(
2125                &plugin,
2126                &ctx,
2127                &session.token,
2128                serde_json::json!({ "name": "wont-expire" }),
2129            )
2130            .await;
2131
2132            // Expire the first key by setting expires_at to the past
2133            let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
2134            ctx.database
2135                .update_api_key(
2136                    &id1,
2137                    UpdateApiKey {
2138                        expires_at: Some(Some(past)),
2139                        ..Default::default()
2140                    },
2141                )
2142                .await
2143                .unwrap();
2144
2145            // Delete expired keys
2146            let deleted = ctx.database.delete_expired_api_keys().await.unwrap();
2147            assert_eq!(deleted, 1, "Should delete exactly 1 expired key");
2148
2149            // Verify only the non-expired key remains
2150            let remaining = ctx
2151                .database
2152                .list_api_keys_by_reference(&fixture_user.id)
2153                .await
2154                .unwrap();
2155            assert_eq!(remaining.len(), 1);
2156        }
2157
2158        // 9. Delete expired: calling the store function directly removes only expired keys
2159        #[tokio::test]
2160        async fn test_delete_expired_removes_only_expired() {
2161            let plugin = ApiKeyPlugin::builder().build();
2162            let (ctx, fixture_user, session) = create_test_context_with_user().await;
2163
2164            // Create two keys, expire one
2165            let (id1, _) = create_key_and_get_raw(
2166                &plugin,
2167                &ctx,
2168                &session.token,
2169                serde_json::json!({ "name": "expired" }),
2170            )
2171            .await;
2172            let (_id2, _) = create_key_and_get_raw(
2173                &plugin,
2174                &ctx,
2175                &session.token,
2176                serde_json::json!({ "name": "active" }),
2177            )
2178            .await;
2179
2180            let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
2181            ctx.database
2182                .update_api_key(
2183                    &id1,
2184                    UpdateApiKey {
2185                        expires_at: Some(Some(past)),
2186                        ..Default::default()
2187                    },
2188                )
2189                .await
2190                .unwrap();
2191
2192            let deleted = ctx.database.delete_expired_api_keys().await.unwrap();
2193            assert_eq!(deleted, 1);
2194
2195            let remaining = ctx
2196                .database
2197                .list_api_keys_by_reference(&fixture_user.id)
2198                .await
2199                .unwrap();
2200            assert_eq!(remaining.len(), 1);
2201        }
2202
2203        // 10. before_request returns None when enableSessionForAPIKeys is false
2204        // Upstream reference: packages/better-auth/src/plugins/api-key/api-key.test.ts :: describe("api-key"); adapted to the Rust API key plugin handlers.
2205        #[tokio::test]
2206        async fn test_before_request_disabled_returns_none() {
2207            let plugin = ApiKeyPlugin::builder().build(); // enable_session_for_api_keys defaults to false
2208            let (ctx, _user, session) = create_test_context_with_user().await;
2209
2210            let (_id, raw_key) = create_key_and_get_raw(
2211                &plugin,
2212                &ctx,
2213                &session.token,
2214                serde_json::json!({ "name": "disabled-session" }),
2215            )
2216            .await;
2217
2218            let mut headers = HashMap::new();
2219            headers.insert("x-api-key".to_owned(), raw_key);
2220            let req = AuthRequest::from_parts(
2221                HttpMethod::Get,
2222                "/get-session".to_owned(),
2223                headers,
2224                None,
2225                HashMap::new(),
2226            );
2227
2228            let action = plugin.before_request(&req, &ctx).await.unwrap();
2229            assert!(
2230                action.is_none(),
2231                "before_request should return None when session emulation is disabled"
2232            );
2233        }
2234
2235        // Upstream reference: @better-auth/api-key :: resolveConfiguration — an absent
2236        // or unknown configId falls back to the default configuration.
2237        #[tokio::test]
2238        async fn test_resolve_configuration_falls_back_to_default() {
2239            let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2240                config_id: "billing".to_owned(),
2241                ..ApiKeyConfig::default()
2242            });
2243
2244            assert_eq!(
2245                plugin.resolve_configuration(None).unwrap().config_id,
2246                "default"
2247            );
2248            assert_eq!(
2249                plugin
2250                    .resolve_configuration(Some("billing"))
2251                    .unwrap()
2252                    .config_id,
2253                "billing"
2254            );
2255            // Unknown ids fall back rather than erroring.
2256            assert_eq!(
2257                plugin
2258                    .resolve_configuration(Some("nope"))
2259                    .unwrap()
2260                    .config_id,
2261                "default"
2262            );
2263        }
2264
2265        // Upstream reference: @better-auth/api-key :: resolveConfiguration errors when
2266        // no configuration is registered as the default.
2267        #[tokio::test]
2268        async fn test_resolve_configuration_without_default_is_an_error() {
2269            let plugin = ApiKeyPlugin::builder()
2270                .config_id("billing".to_owned())
2271                .build();
2272
2273            let err = plugin.resolve_configuration(None).unwrap_err();
2274            assert_eq!(err.status_code(), 400);
2275            assert_eq!(err.to_string(), "No default api-key configuration found.");
2276        }
2277
2278        // Upstream reference: @better-auth/api-key :: configIdMatches treats a missing
2279        // configId as the default, for keys written before the column existed.
2280        #[tokio::test]
2281        async fn test_config_id_matches_treats_missing_as_default() {
2282            assert!(config_id_matches("", "default"));
2283            assert!(config_id_matches("default", ""));
2284            assert!(config_id_matches("billing", "billing"));
2285            assert!(!config_id_matches("billing", "default"));
2286        }
2287
2288        // Upstream reference: @better-auth/api-key :: create with `references:
2289        // "organization"` requires organizationId.
2290        #[tokio::test]
2291        async fn test_create_for_organization_requires_organization_id() {
2292            let plugin = ApiKeyPlugin::builder()
2293                .references(ApiKeyReferences::Organization)
2294                .build();
2295            let (ctx, _user, session) = create_test_context_with_user().await;
2296
2297            let req = create_auth_request(
2298                HttpMethod::Post,
2299                "/api-key/create",
2300                Some(&session.token),
2301                Some(serde_json::json!({ "name": "org-key" })),
2302                None,
2303            );
2304            let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2305
2306            assert_eq!(err.status_code(), 400);
2307            assert_eq!(
2308                err.to_string(),
2309                "Organization ID is required for organization-owned API keys."
2310            );
2311        }
2312
2313        // Upstream reference: @better-auth/api-key :: checkOrgApiKeyPermission fails
2314        // when the organization plugin, which supplies the access control, is absent.
2315        #[tokio::test]
2316        async fn test_create_for_organization_requires_the_organization_plugin() {
2317            let plugin = ApiKeyPlugin::builder()
2318                .references(ApiKeyReferences::Organization)
2319                .build();
2320            let (ctx, _user, session) = create_test_context_with_user().await;
2321
2322            let req = create_auth_request(
2323                HttpMethod::Post,
2324                "/api-key/create",
2325                Some(&session.token),
2326                Some(serde_json::json!({ "name": "org-key", "organizationId": "org-1" })),
2327                None,
2328            );
2329            let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2330
2331            assert_eq!(
2332                err.to_string(),
2333                "Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
2334            );
2335        }
2336
2337        // Upstream reference: @better-auth/api-key :: checkOrgApiKeyPermission rejects
2338        // a caller who is not a member of the owning organization.
2339        #[tokio::test]
2340        async fn test_create_for_organization_rejects_non_member() {
2341            let plugin = ApiKeyPlugin::builder()
2342                .references(ApiKeyReferences::Organization)
2343                .build();
2344            let (ctx, _user, session) = create_test_context_with_user().await;
2345
2346            // Stand in for a registered organization plugin.
2347            let mut metadata: HashMap<String, serde_json::Value> = HashMap::new();
2348            _ = metadata.insert(
2349                crate::organization::METADATA_ENABLED.to_owned(),
2350                serde_json::Value::Bool(true),
2351            );
2352            let ctx = AuthContext::with_metadata(
2353                Arc::clone(&ctx.config),
2354                Arc::clone(&ctx.database),
2355                metadata,
2356            );
2357
2358            let req = create_auth_request(
2359                HttpMethod::Post,
2360                "/api-key/create",
2361                Some(&session.token),
2362                Some(
2363                    serde_json::json!({ "name": "org-key", "organizationId": "org-the-user-is-not-in" }),
2364                ),
2365                None,
2366            );
2367            let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2368
2369            assert_eq!(
2370                err.to_string(),
2371                "You are not a member of the organization that owns this API key."
2372            );
2373        }
2374    }
2375
2376    mod verification_tests {
2377        use super::*;
2378
2379        #[tokio::test]
2380        async fn scoped_verification_uses_the_configuration_hashing_and_rejects_other_configs() {
2381            let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2382                config_id: "machines".to_owned(),
2383                disable_key_hashing: true,
2384                ..Default::default()
2385            });
2386            let (ctx, _, session) = create_test_context_with_user().await;
2387            let (id, key) = create_key_and_get_raw(
2388                &plugin,
2389                &ctx,
2390                &session.token,
2391                serde_json::json!({ "configId": "machines", "name": "machine" }),
2392            )
2393            .await;
2394            let scoped = VerifyApiKey {
2395                key: &key,
2396                config_id: Some("machines"),
2397                permissions: None,
2398            };
2399            let verified = plugin.verify_api_key(&scoped, &ctx).await.unwrap();
2400            assert_eq!(verified.id, id);
2401            assert_eq!(verified.config_id, "machines");
2402            let serialized = serde_json::to_value(verified).unwrap();
2403            assert!(serialized.get("key").is_none());
2404            assert!(serialized.get("keyHash").is_none());
2405
2406            for config_id in [None, Some("default"), Some("unknown")] {
2407                let error = plugin
2408                    .verify_api_key(
2409                        &VerifyApiKey {
2410                            config_id,
2411                            ..scoped
2412                        },
2413                        &ctx,
2414                    )
2415                    .await
2416                    .unwrap_err();
2417                assert!(matches!(
2418                    error,
2419                    ApiKeyVerificationError::Validation(ApiKeyValidationError {
2420                        code: ApiKeyErrorCode::InvalidApiKey,
2421                        ..
2422                    })
2423                ));
2424            }
2425            assert_eq!(
2426                ctx.database
2427                    .get_api_key_by_id(&id)
2428                    .await
2429                    .unwrap()
2430                    .unwrap()
2431                    .request_count,
2432                Some(1.0)
2433            );
2434        }
2435
2436        #[tokio::test]
2437        async fn unscoped_verification_uses_the_issuing_configuration_limits() {
2438            let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2439                config_id: "machines".to_owned(),
2440                rate_limit: RateLimitDefaults {
2441                    enabled: false,
2442                    ..Default::default()
2443                },
2444                ..Default::default()
2445            });
2446            let (ctx, _, session) = create_test_context_with_user().await;
2447            let (id, key) = create_key_and_get_raw(
2448                &plugin,
2449                &ctx,
2450                &session.token,
2451                serde_json::json!({ "configId": "machines" }),
2452            )
2453            .await;
2454            ctx.database
2455                .update_api_key(
2456                    &id,
2457                    UpdateApiKey {
2458                        rate_limit_enabled: Some(true),
2459                        rate_limit_max: Some(1.0),
2460                        ..Default::default()
2461                    },
2462                )
2463                .await
2464                .unwrap();
2465
2466            let input = VerifyApiKey {
2467                key: &key,
2468                config_id: None,
2469                permissions: None,
2470            };
2471            for _ in 0..3 {
2472                assert_eq!(plugin.verify_api_key(&input, &ctx).await.unwrap().id, id);
2473            }
2474            let error = plugin
2475                .verify_api_key(
2476                    &VerifyApiKey {
2477                        config_id: Some("default"),
2478                        ..input
2479                    },
2480                    &ctx,
2481                )
2482                .await
2483                .unwrap_err();
2484            assert!(matches!(
2485                error,
2486                ApiKeyVerificationError::Validation(ApiKeyValidationError {
2487                    code: ApiKeyErrorCode::InvalidApiKey,
2488                    ..
2489                })
2490            ));
2491            assert_eq!(
2492                ctx.database
2493                    .get_api_key_by_id(&id)
2494                    .await
2495                    .unwrap()
2496                    .unwrap()
2497                    .request_count,
2498                Some(0.0)
2499            );
2500        }
2501
2502        #[tokio::test]
2503        async fn permissions_failure_preserves_usage_and_rate_limit_reports_retry_time() {
2504            let plugin = ApiKeyPlugin::builder().build();
2505            let (ctx, _, session) = create_test_context_with_user().await;
2506            let (id, key) = create_key_with_server_fields(
2507                &plugin,
2508                &ctx,
2509                &session.token,
2510                serde_json::json!({}),
2511                UpdateApiKey {
2512                    remaining: Some(3.0),
2513                    permissions: Some(serde_json::json!({ "nodes": ["read"] }).to_string()),
2514                    rate_limit_max: Some(1.0),
2515                    rate_limit_time_window: Some(60_000.0),
2516                    ..Default::default()
2517                },
2518            )
2519            .await;
2520            let denied = serde_json::json!({ "nodes": ["delete"] });
2521            let allowed = serde_json::json!({ "nodes": ["read"] });
2522            let input = VerifyApiKey {
2523                key: &key,
2524                config_id: None,
2525                permissions: Some(&denied),
2526            };
2527            let error = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
2528            assert!(matches!(
2529                error,
2530                ApiKeyVerificationError::Validation(ApiKeyValidationError {
2531                    code: ApiKeyErrorCode::KeyNotFound,
2532                    ..
2533                })
2534            ));
2535            assert_eq!(
2536                ctx.database
2537                    .get_api_key_by_id(&id)
2538                    .await
2539                    .unwrap()
2540                    .unwrap()
2541                    .remaining,
2542                Some(3.0)
2543            );
2544            let input = VerifyApiKey {
2545                permissions: Some(&allowed),
2546                ..input
2547            };
2548            assert_eq!(
2549                plugin.verify_api_key(&input, &ctx).await.unwrap().remaining,
2550                Some(2.0)
2551            );
2552            let error_2 = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
2553            let ApiKeyVerificationError::Validation(error_2_3) = error_2 else {
2554                panic!("Expected rate limit rejection")
2555            };
2556            assert_eq!(error_2_3.code, ApiKeyErrorCode::RateLimited);
2557            let retry = error_2_3.details.unwrap().try_again_in;
2558            assert!(retry > 0.0 && retry <= 60_000.0);
2559            assert_eq!(
2560                ctx.database
2561                    .get_api_key_by_id(&id)
2562                    .await
2563                    .unwrap()
2564                    .unwrap()
2565                    .remaining,
2566                Some(1.0)
2567            );
2568        }
2569
2570        #[tokio::test]
2571        async fn session_header_selects_a_named_config_without_a_default() {
2572            let plugin = ApiKeyPlugin::builder()
2573                .config_id("machines".to_owned())
2574                .api_key_headers(vec![
2575                    "x-machine-key".to_owned(),
2576                    "x-alternate-key".to_owned(),
2577                ])
2578                .enable_session_for_api_keys(true)
2579                .build();
2580            let (ctx, user, session) = create_test_context_with_user().await;
2581            let (_, key) = create_key_and_get_raw(
2582                &plugin,
2583                &ctx,
2584                &session.token,
2585                serde_json::json!({ "configId": "machines" }),
2586            )
2587            .await;
2588            let mut request =
2589                create_auth_request(HttpMethod::Get, "/get-session", None, None, None);
2590            request
2591                .headers
2592                .insert("x-alternate-key".to_owned(), key.clone());
2593            let result = plugin
2594                .before_request(&request, &ctx)
2595                .await
2596                .unwrap()
2597                .unwrap();
2598            let BeforeRequestAction::Respond(response) = result else {
2599                panic!("Expected virtual session")
2600            };
2601            assert_eq!(response.status, 200);
2602            let body = json_body(&response);
2603            assert_eq!(
2604                (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2605                    .get("token")
2606                    .unwrap_or(&serde_json::Value::Null)),
2607                key
2608            );
2609            assert_eq!(
2610                (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2611                    .get("userId")
2612                    .unwrap_or(&serde_json::Value::Null)),
2613                user.id
2614            );
2615            assert_eq!(
2616                (*(*(body).get("user").unwrap_or(&serde_json::Value::Null))
2617                    .get("emailVerified")
2618                    .unwrap_or(&serde_json::Value::Null)),
2619                false
2620            );
2621            assert!(
2622                (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2623                    .get("createdAt")
2624                    .unwrap_or(&serde_json::Value::Null))
2625                .is_string()
2626            );
2627            assert!(
2628                (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2629                    .get("expiresAt")
2630                    .unwrap_or(&serde_json::Value::Null))
2631                .is_string()
2632            );
2633        }
2634
2635        #[tokio::test]
2636        async fn session_header_cannot_authenticate_a_different_configuration() {
2637            let plugin = ApiKeyPlugin::builder()
2638                .enable_session_for_api_keys(true)
2639                .build()
2640                .configuration(ApiKeyConfig {
2641                    config_id: "machines".to_owned(),
2642                    api_key_headers: vec!["x-machine-key".to_owned()],
2643                    enable_session_for_api_keys: true,
2644                    ..Default::default()
2645                });
2646            let (ctx, _, session) = create_test_context_with_user().await;
2647            let (id, key) = create_key_and_get_raw(
2648                &plugin,
2649                &ctx,
2650                &session.token,
2651                serde_json::json!({ "configId": "machines" }),
2652            )
2653            .await;
2654            let mut request =
2655                create_auth_request(HttpMethod::Get, "/get-session", None, None, None);
2656            request.headers.insert("x-api-key".to_owned(), key);
2657            let result = plugin
2658                .before_request(&request, &ctx)
2659                .await
2660                .unwrap()
2661                .unwrap();
2662            let BeforeRequestAction::Respond(response) = result else {
2663                panic!("Expected credential rejection")
2664            };
2665            assert_eq!(response.status, 401);
2666            assert_eq!(
2667                (*(json_body(&response))
2668                    .get("code")
2669                    .unwrap_or(&serde_json::Value::Null)),
2670                "INVALID_API_KEY"
2671            );
2672            assert_eq!(
2673                ctx.database
2674                    .get_api_key_by_id(&id)
2675                    .await
2676                    .unwrap()
2677                    .unwrap()
2678                    .request_count,
2679                Some(0.0)
2680            );
2681        }
2682
2683        #[tokio::test]
2684        async fn organization_key_verifies_without_emulating_a_user_session() {
2685            let plugin = ApiKeyPlugin::builder()
2686                .references(ApiKeyReferences::Organization)
2687                .enable_session_for_api_keys(true)
2688                .build();
2689            let (ctx, user, _) = create_test_context_with_user().await;
2690            let (key, key_hash, start) =
2691                ApiKeyPlugin::generate_key(&ApiKeyConfig::default(), None).unwrap();
2692            ctx.database
2693                .create_api_key(alibi_core::CreateApiKey {
2694                    // A colliding user ID must not turn an organization key into a user session.
2695                    reference_id: user.id,
2696                    config_id: "default".to_owned(),
2697                    key_hash,
2698                    start: Some(start),
2699                    enabled: true,
2700                    name: None,
2701                    prefix: None,
2702                    expires_at: None,
2703                    remaining: None,
2704                    rate_limit_enabled: false,
2705                    rate_limit_time_window: None,
2706                    rate_limit_max: None,
2707                    refill_interval: None,
2708                    refill_amount: None,
2709                    permissions: None,
2710                    metadata: None,
2711                })
2712                .await
2713                .unwrap();
2714            plugin
2715                .verify_api_key(
2716                    &VerifyApiKey {
2717                        key: &key,
2718                        config_id: None,
2719                        permissions: None,
2720                    },
2721                    &ctx,
2722                )
2723                .await
2724                .unwrap();
2725            let mut request =
2726                create_auth_request(HttpMethod::Get, "/get-session", None, None, None);
2727            request.headers.insert("x-api-key".to_owned(), key);
2728            let result = plugin
2729                .before_request(&request, &ctx)
2730                .await
2731                .unwrap()
2732                .unwrap();
2733            let BeforeRequestAction::Respond(response) = result else {
2734                panic!("Expected organization rejection")
2735            };
2736            assert_eq!(response.status, 401);
2737            assert_eq!(
2738                (*(json_body(&response))
2739                    .get("code")
2740                    .unwrap_or(&serde_json::Value::Null)),
2741                "INVALID_REFERENCE_ID_FROM_API_KEY"
2742            );
2743        }
2744
2745        #[tokio::test]
2746        async fn api_key_initialization_rejects_ambiguous_configurations() {
2747            let (ctx, _, _) = create_test_context_with_user().await;
2748            for config_id in ["", "default"] {
2749                let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2750                    config_id: config_id.to_owned(),
2751                    ..Default::default()
2752                });
2753                let mut init = alibi_core::AuthInitContext::new(
2754                    Arc::clone(&ctx.config),
2755                    Arc::clone(&ctx.database),
2756                );
2757                assert!(matches!(
2758                    plugin.on_init(&mut init).await,
2759                    Err(AuthError::Config(_))
2760                ));
2761            }
2762        }
2763
2764        #[tokio::test]
2765        async fn verification_preserves_database_failures() {
2766            let connection = alibi_seaorm::Database::connect("sqlite::memory:")
2767                .await
2768                .unwrap();
2769            connection.clone().close().await.unwrap();
2770            let config = Arc::new(crate::test_helpers::create_test_config());
2771            let database = Arc::new(alibi_seaorm::SeaOrmStore::<TestSchema>::new(
2772                Arc::clone(&config),
2773                connection,
2774            ));
2775            let ctx = AuthContext::new(config, database);
2776            let plugin = ApiKeyPlugin::builder().build();
2777            let error = plugin
2778                .verify_api_key(
2779                    &VerifyApiKey {
2780                        key: "not-looked-up",
2781                        config_id: None,
2782                        permissions: None,
2783                    },
2784                    &ctx,
2785                )
2786                .await
2787                .unwrap_err();
2788            assert!(matches!(
2789                error,
2790                ApiKeyVerificationError::Internal(AuthError::Database(_))
2791            ));
2792        }
2793
2794        #[tokio::test]
2795        async fn verified_session_authenticates_a_protected_plugin_route_without_a_database_session()
2796         {
2797            let plugin = ApiKeyPlugin::builder()
2798                .enable_session_for_api_keys(true)
2799                .build();
2800            let (ctx, user, session) = create_test_context_with_user().await;
2801            let (id, key) =
2802                create_key_and_get_raw(&plugin, &ctx, &session.token, serde_json::json!({})).await;
2803            let mut request =
2804                create_auth_request(HttpMethod::Get, "/api-key/list", None, None, None);
2805            request.headers.insert("x-api-key".to_owned(), key.clone());
2806            let action = plugin
2807                .before_request(&request, &ctx)
2808                .await
2809                .unwrap()
2810                .unwrap();
2811            let BeforeRequestAction::InjectSession { session: session_2 } = action else {
2812                panic!("Expected virtual session")
2813            };
2814            assert_eq!(session_2.token, key);
2815            request.set_virtual_session(session_2);
2816            let response = plugin.on_request(&request, &ctx).await.unwrap().unwrap();
2817            assert_eq!(response.status, 200);
2818            assert_eq!(
2819                (*(*(*(json_body(&response))
2820                    .get("apiKeys")
2821                    .unwrap_or(&serde_json::Value::Null))
2822                .get(0)
2823                .unwrap_or(&serde_json::Value::Null))
2824                .get("id")
2825                .unwrap_or(&serde_json::Value::Null)),
2826                id
2827            );
2828            assert_eq!(
2829                ctx.database
2830                    .get_user_sessions(&user.id)
2831                    .await
2832                    .unwrap()
2833                    .len(),
2834                1
2835            );
2836        }
2837
2838        // The SDK uses an actual HTTP request. This guards the separate public Rust
2839        // contract: server-only verification without a request can use typed application
2840        // policy from immutable context extensions and preserves quota on rejection.
2841        #[tokio::test]
2842        async fn programmatic_validator_uses_typed_policy_without_a_request() {
2843            use std::sync::atomic::{AtomicBool, Ordering};
2844            struct Policy(AtomicBool);
2845            struct Predicate {
2846                private_policy_secret: String,
2847            }
2848            #[async_trait::async_trait]
2849            impl ApiKeyValidator for Predicate {
2850                async fn validate(
2851                    &self,
2852                    context: &ApiKeyCallbackContext<'_>,
2853                    _key: &str,
2854                ) -> AuthResult<bool> {
2855                    Ok(context.request.is_none()
2856                        && context.configuration_id == "programmatic"
2857                        && context.auth_config.secret == "test-secret-key-at-least-32-chars-long"
2858                        && self.private_policy_secret == "application-private-policy-secret"
2859                        && context
2860                            .extensions
2861                            .get::<Policy>()
2862                            .is_some_and(|policy| policy.0.load(Ordering::SeqCst)))
2863                }
2864            }
2865            let (mut ctx, user, _) = create_test_context_with_user().await;
2866            ctx.extensions.insert(Policy(AtomicBool::new(false)));
2867            let configuration = ApiKeyConfig {
2868                config_id: "programmatic".to_owned(),
2869                custom_api_key_validator: Some(Arc::new(Predicate {
2870                    private_policy_secret: "application-private-policy-secret".to_owned(),
2871                })),
2872                rate_limit: RateLimitDefaults {
2873                    enabled: false,
2874                    ..Default::default()
2875                },
2876                ..Default::default()
2877            };
2878            assert!(!format!("{configuration:?}").contains("application-private-policy-secret"));
2879            let plugin = ApiKeyPlugin::with_config(configuration);
2880            let created = plugin
2881                .create_key(
2882                    &ctx,
2883                    &CreateKeyRequest {
2884                        config_id: Some("programmatic".to_owned()),
2885                        user_id: Some(user.id.clone()),
2886                        remaining: Some(2.0),
2887                        ..Default::default()
2888                    },
2889                )
2890                .await
2891                .unwrap();
2892            let input = VerifyApiKey {
2893                key: &created.key,
2894                config_id: Some("programmatic"),
2895                permissions: None,
2896            };
2897            let rejected = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
2898            assert!(matches!(
2899                rejected,
2900                ApiKeyVerificationError::Validation(ApiKeyValidationError {
2901                    code: ApiKeyErrorCode::KeyNotFound,
2902                    ..
2903                })
2904            ));
2905            assert_eq!(
2906                ctx.database
2907                    .get_api_key_by_id(&created.api_key.id)
2908                    .await
2909                    .unwrap()
2910                    .unwrap()
2911                    .remaining,
2912                Some(2.0)
2913            );
2914            ctx.extensions
2915                .get::<Policy>()
2916                .unwrap()
2917                .0
2918                .store(true, Ordering::SeqCst);
2919            let accepted = plugin.verify_api_key(&input, &ctx).await.unwrap();
2920            assert_eq!(accepted.reference_id, user.id);
2921            assert_eq!(accepted.id, created.api_key.id);
2922            assert_eq!(accepted.remaining, Some(1.0));
2923            assert_eq!(
2924                ctx.database
2925                    .get_api_key_by_id(&created.api_key.id)
2926                    .await
2927                    .unwrap()
2928                    .unwrap()
2929                    .remaining,
2930                Some(1.0)
2931            );
2932        }
2933    }
2934}
2935// LCOV_EXCL_STOP
2936
2937// LCOV_EXCL_START
2938#[cfg(test)]
2939mod crud_tests {
2940    use super::*;
2941    use alibi_core::{AuthConfig, CreateSession, CreateUser, HttpMethod};
2942    use chrono::{Duration, Utc};
2943    use serde_json::json;
2944    use std::collections::HashMap;
2945    use std::sync::Arc;
2946
2947    type TestSchema = alibi_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
2948
2949    async fn context() -> (AuthContext<TestSchema>, String, String) {
2950        let database = crate::test_helpers::create_test_database().await;
2951        let user = database
2952            .create_user(
2953                CreateUser::new()
2954                    .with_email("crud@example.com")
2955                    .with_name("CRUD user"),
2956            )
2957            .await
2958            .unwrap();
2959        let user_id = user.id().to_string();
2960        let session = database
2961            .create_session(CreateSession {
2962                additional_fields: alibi_core::field_policy::FieldValues::default(),
2963                token: None,
2964                active_team_id: None,
2965                user_id: user_id.clone(),
2966                expires_at: Utc::now() + Duration::hours(1),
2967                ip_address: None,
2968                user_agent: None,
2969                impersonated_by: None,
2970                active_organization_id: None,
2971            })
2972            .await
2973            .unwrap();
2974        let token = alibi_core::entity::AuthSession::token(&session).to_owned();
2975        (
2976            AuthContext::new(
2977                Arc::new(AuthConfig::new("a-secret-that-is-at-least-32-characters")),
2978                database,
2979            ),
2980            user_id,
2981            token,
2982        )
2983    }
2984
2985    fn request(token: &str, path: &str, body: &serde_json::Value) -> AuthRequest {
2986        AuthRequest::from_parts(
2987            HttpMethod::Post,
2988            path.to_owned(),
2989            HashMap::from([(
2990                "cookie".to_owned(),
2991                format!(
2992                    "better-auth.session_token={}",
2993                    alibi_core::utils::cookie_utils::sign_cookie_value(
2994                        token,
2995                        "a-secret-that-is-at-least-32-characters"
2996                    )
2997                ),
2998            )]),
2999            Some(serde_json::to_vec(&body).unwrap()),
3000            HashMap::new(),
3001        )
3002    }
3003
3004    async fn server_key(
3005        plugin: &ApiKeyPlugin,
3006        ctx: &AuthContext<TestSchema>,
3007        user_id: &str,
3008        config_id: &str,
3009    ) -> CreateKeyResponse {
3010        plugin
3011            .create_key(
3012                ctx,
3013                &CreateKeyRequest {
3014                    user_id: Some(user_id.to_owned()),
3015                    config_id: Some(config_id.to_owned()),
3016                    ..Default::default()
3017                },
3018            )
3019            .await
3020            .unwrap()
3021    }
3022
3023    #[tokio::test]
3024    async fn list_without_configuration_includes_all_user_configurations() {
3025        let (ctx, user_id, _) = context().await;
3026        let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
3027            config_id: "secondary".to_owned(),
3028            ..Default::default()
3029        });
3030        server_key(&plugin, &ctx, &user_id, "default").await;
3031        server_key(&plugin, &ctx, &user_id, "secondary").await;
3032        let list = list_keys_core(&user_id, &ListKeysQuery::default(), &plugin, &ctx)
3033            .await
3034            .unwrap();
3035        assert_eq!(list.total, 2);
3036        let list_2 = list_keys_core(
3037            &user_id,
3038            &ListKeysQuery {
3039                config_id: Some("secondary".to_owned()),
3040                ..Default::default()
3041            },
3042            &plugin,
3043            &ctx,
3044        )
3045        .await
3046        .unwrap();
3047        assert_eq!(list_2.total, 1);
3048        assert_eq!(
3049            (list_2.api_keys)
3050                .first()
3051                .expect("fixture contains the requested index")
3052                .config_id,
3053            "secondary"
3054        );
3055        let list_3 = list_keys_core(
3056            &user_id,
3057            &ListKeysQuery {
3058                config_id: Some("missing".to_owned()),
3059                ..Default::default()
3060            },
3061            &plugin,
3062            &ctx,
3063        )
3064        .await
3065        .unwrap();
3066        assert_eq!(list_3.total, 0);
3067
3068        let no_default = ApiKeyPlugin::builder()
3069            .config_id("secondary".to_owned())
3070            .build();
3071        assert_eq!(
3072            list_keys_core(&user_id, &ListKeysQuery::default(), &no_default, &ctx)
3073                .await
3074                .unwrap()
3075                .total,
3076            2
3077        );
3078    }
3079
3080    #[tokio::test]
3081    async fn http_requests_cannot_impersonate_users_or_change_server_permissions() {
3082        let (ctx, user_id, token) = context().await;
3083        let plugin = ApiKeyPlugin::builder().build();
3084        let create = request(&token, "/api-key/create", &(json!({"userId":user_id})));
3085        assert_eq!(
3086            plugin
3087                .handle_create(&create, &ctx)
3088                .await
3089                .unwrap_err()
3090                .status_code(),
3091            401
3092        );
3093        let key = server_key(&plugin, &ctx, &user_id, "default").await;
3094        let update = request(
3095            &token,
3096            "/api-key/update",
3097            &(json!({"keyId":key.api_key.id,"userId":"someone-else","enabled":false})),
3098        );
3099        assert_eq!(
3100            plugin
3101                .handle_update(&update, &ctx)
3102                .await
3103                .unwrap_err()
3104                .status_code(),
3105            401
3106        );
3107        let update_2 = request(
3108            &token,
3109            "/api-key/update",
3110            &(json!({"keyId":key.api_key.id,"permissions":null})),
3111        );
3112        assert_eq!(
3113            plugin
3114                .handle_update(&update_2, &ctx)
3115                .await
3116                .unwrap_err()
3117                .to_string(),
3118            ApiKeyErrorCode::ServerOnlyProperty.message()
3119        );
3120        let update_3 = request(&token, "/api-key/update", &(json!({"keyId":"missing"})));
3121        assert_eq!(
3122            plugin
3123                .handle_update(&update_3, &ctx)
3124                .await
3125                .unwrap_err()
3126                .status_code(),
3127            404
3128        );
3129    }
3130
3131    #[tokio::test]
3132    async fn metadata_can_be_cleared_and_disabled_metadata_is_ignored_on_update() {
3133        let (ctx, user_id, token) = context().await;
3134        let enabled = ApiKeyPlugin::builder().enable_metadata(true).build();
3135        let key = enabled
3136            .create_key(
3137                &ctx,
3138                &CreateKeyRequest {
3139                    user_id: Some(user_id.clone()),
3140                    metadata: Some(json!(["initial"]).into()),
3141                    ..Default::default()
3142                },
3143            )
3144            .await
3145            .unwrap();
3146        assert_eq!(key.api_key.metadata, Some(json!(["initial"])));
3147        let update = request(
3148            &token,
3149            "/api-key/update",
3150            &(json!({"keyId":key.api_key.id,"metadata":null})),
3151        );
3152        let response = enabled.handle_update(&update, &ctx).await.unwrap();
3153        assert!(
3154            (*(serde_json::from_slice::<serde_json::Value>(&response.body).unwrap())
3155                .get("metadata")
3156                .unwrap_or(&serde_json::Value::Null))
3157            .is_null()
3158        );
3159        let disabled = ApiKeyPlugin::builder().build();
3160        let update_2 = request(
3161            &token,
3162            "/api-key/update",
3163            &(json!({"keyId":key.api_key.id,"metadata":{"ignored":true},"enabled":false})),
3164        );
3165        let response_2 = disabled.handle_update(&update_2, &ctx).await.unwrap();
3166        let result: serde_json::Value = serde_json::from_slice(&response_2.body).unwrap();
3167        assert!((*(result).get("metadata").unwrap_or(&serde_json::Value::Null)).is_null());
3168        assert_eq!(
3169            (*(result).get("enabled").unwrap_or(&serde_json::Value::Null)),
3170            false
3171        );
3172        let update_3 = request(
3173            &token,
3174            "/api-key/update",
3175            &(json!({"keyId":key.api_key.id,"metadata":null})),
3176        );
3177        assert_eq!(
3178            disabled
3179                .handle_update(&update_3, &ctx)
3180                .await
3181                .unwrap_err()
3182                .to_string(),
3183            ApiKeyErrorCode::NoValuesToUpdate.message()
3184        );
3185    }
3186
3187    #[tokio::test]
3188    async fn trusted_creation_and_update_preserve_permissions_and_fractional_expiration() {
3189        let (ctx, user_id, _) = context().await;
3190        let plugin = ApiKeyPlugin::builder()
3191            .key_expiration(KeyExpirationConfig {
3192                min_expires_in: 0.0,
3193                ..Default::default()
3194            })
3195            .build();
3196        let before = Utc::now();
3197        let key = plugin
3198            .create_key(
3199                &ctx,
3200                &CreateKeyRequest {
3201                    user_id: Some(user_id.clone()),
3202                    expires_in: Some(86400.25),
3203                    remaining: Some(3.0),
3204                    permissions: Some(ApiKeyPermissions::from([(
3205                        "device".to_owned(),
3206                        vec!["read".to_owned()],
3207                    )])),
3208                    ..Default::default()
3209                },
3210            )
3211            .await
3212            .unwrap();
3213        assert_eq!(key.api_key.remaining, Some(3.0));
3214        assert_eq!(key.api_key.permissions, Some(json!({"device":["read"]})));
3215        let expires_at =
3216            chrono::DateTime::parse_from_rfc3339(key.api_key.expires_at.as_deref().unwrap())
3217                .unwrap()
3218                .with_timezone(&Utc);
3219        assert!((expires_at - before).num_milliseconds() >= 86_400_249);
3220        assert!((expires_at - Utc::now()).num_milliseconds() <= 86_400_250);
3221        let updated = plugin
3222            .update_key(
3223                &ctx,
3224                &UpdateKeyRequest {
3225                    key_id: key.api_key.id.clone(),
3226                    user_id: Some(user_id),
3227                    remaining: Some(5.0),
3228                    permissions: Some(None),
3229                    expires_in: Some(None),
3230                    ..Default::default()
3231                },
3232            )
3233            .await
3234            .unwrap();
3235        assert_eq!(updated.remaining, Some(5.0));
3236        assert_eq!(updated.permissions, Some(serde_json::Value::Null));
3237        assert_eq!(updated.expires_at, None);
3238        let stored = ctx
3239            .database
3240            .get_api_key_by_id(&key.api_key.id)
3241            .await
3242            .unwrap()
3243            .unwrap();
3244        assert_ne!(stored.key_hash, key.key);
3245    }
3246
3247    #[tokio::test]
3248    async fn list_rejects_invalid_pagination_instead_of_ignoring_it() {
3249        let (ctx, _, token) = context().await;
3250        let plugin = ApiKeyPlugin::builder().build();
3251        for (field, value) in [
3252            ("limit", "-1"),
3253            ("offset", "1.5"),
3254            ("limit", "bad"),
3255            ("sortDirection", "sideways"),
3256        ] {
3257            let mut req = request(&token, "/api-key/list", &(json!({})));
3258            req.query.insert(field.to_owned(), value.to_owned());
3259            let response = plugin.handle_list(&req, &ctx).await.unwrap();
3260            assert_eq!(response.status, 400);
3261            assert_eq!(
3262                (*(serde_json::from_slice::<serde_json::Value>(&response.body).unwrap())
3263                    .get("code")
3264                    .unwrap_or(&serde_json::Value::Null)),
3265                "VALIDATION_ERROR"
3266            );
3267        }
3268    }
3269
3270    #[tokio::test]
3271    async fn forced_cleanup_preserves_rows_on_store_failure_and_retries_without_throttle() {
3272        use alibi_seaorm::sea_orm::{ConnectionTrait, DbBackend, Statement};
3273        let config = Arc::new(AuthConfig::new("forced-cleanup-application-secret32"));
3274        let connection = alibi_seaorm::Database::connect("sqlite::memory:")
3275            .await
3276            .unwrap();
3277        alibi_seaorm::store::__private_test_support::migrator::run_migrations(&connection)
3278            .await
3279            .unwrap();
3280        let database = Arc::new(alibi_seaorm::SeaOrmStore::<TestSchema>::new(
3281            std::sync::Arc::clone(&config),
3282            connection.clone(),
3283        ));
3284        let ctx = AuthContext::new(config, database);
3285        let owner = ctx
3286            .database
3287            .create_user(CreateUser::new().with_email("cleanup@native.local"))
3288            .await
3289            .unwrap();
3290        let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3291            key_expiration: KeyExpirationConfig {
3292                min_expires_in: 0.0,
3293                ..Default::default()
3294            },
3295            ..Default::default()
3296        });
3297        let key = plugin
3298            .create_key(
3299                &ctx,
3300                &CreateKeyRequest {
3301                    user_id: Some(owner.id().to_string()),
3302                    name: Some("expired".into()),
3303                    ..Default::default()
3304                },
3305            )
3306            .await
3307            .unwrap();
3308        let expired = ctx
3309            .database
3310            .update_api_key(
3311                &key.api_key.id,
3312                alibi_core::UpdateApiKey {
3313                    expires_at: Some(Some("1970-01-01T00:00:00.000Z".into())),
3314                    ..Default::default()
3315                },
3316            )
3317            .await
3318            .unwrap();
3319        connection.execute_raw(Statement::from_string(DbBackend::Sqlite,"CREATE TRIGGER reject_api_key_cleanup BEFORE DELETE ON \"apikey\" BEGIN SELECT RAISE(ABORT,'application cleanup rejected'); END")).await.unwrap();
3320        let failed = plugin.delete_all_expired_api_keys(&ctx).await;
3321        assert!(failed.success);
3322        assert!(failed.error.is_none());
3323        let retained = ctx
3324            .database
3325            .get_api_key_by_id(&key.api_key.id)
3326            .await
3327            .unwrap()
3328            .unwrap();
3329        assert_eq!(
3330            alibi_core::utils::json::to_value(&retained).unwrap(),
3331            alibi_core::utils::json::to_value(&expired).unwrap()
3332        );
3333        connection
3334            .execute_raw(Statement::from_string(
3335                DbBackend::Sqlite,
3336                "DROP TRIGGER reject_api_key_cleanup",
3337            ))
3338            .await
3339            .unwrap();
3340        let retry = plugin.delete_all_expired_api_keys(&ctx).await;
3341        assert!(retry.success);
3342        assert!(retry.error.is_none());
3343        assert!(
3344            ctx.database
3345                .get_api_key_by_id(&key.api_key.id)
3346                .await
3347                .unwrap()
3348                .is_none()
3349        );
3350        assert_eq!(
3351            ctx.database
3352                .get_user_by_id(owner.id().as_ref())
3353                .await
3354                .unwrap()
3355                .unwrap(),
3356            owner
3357        );
3358    }
3359    struct ApplicationKeyStorage {
3360        cache: Arc<alibi_core::store::MemoryCacheAdapter>,
3361        fail_writes: std::sync::atomic::AtomicBool,
3362    }
3363    #[async_trait::async_trait]
3364    impl ApiKeyStorage for ApplicationKeyStorage {
3365        async fn get(&self, key: &str) -> AuthResult<Option<String>> {
3366            use alibi_core::store::CacheAdapter as _;
3367            self.cache.get(key).await
3368        }
3369        async fn set(&self, key: &str, value: &str, ttl: Option<Duration>) -> AuthResult<()> {
3370            use alibi_core::store::CacheAdapter as _;
3371            if self.fail_writes.load(std::sync::atomic::Ordering::SeqCst) {
3372                return Err(AuthError::internal("application storage unavailable"));
3373            }
3374            match ttl {
3375                Some(ttl) => self.cache.set(key, value, ttl).await,
3376                None => self.cache.set_without_expiry(key, value).await,
3377            }
3378        }
3379        async fn delete(&self, key: &str) -> AuthResult<()> {
3380            use alibi_core::store::CacheAdapter as _;
3381            self.cache.delete(key).await
3382        }
3383    }
3384    #[derive(Default)]
3385    struct ApplicationCompletions(std::sync::Mutex<Vec<alibi_core::BackgroundTaskCompletion>>);
3386    impl alibi_core::BackgroundTaskHandler for ApplicationCompletions {
3387        fn handle(&self, completion: alibi_core::BackgroundTaskCompletion) -> AuthResult<()> {
3388            self.0.lock().unwrap().push(completion);
3389            Ok(())
3390        }
3391    }
3392    impl ApplicationCompletions {
3393        async fn drain(&self) {
3394            let tasks = std::mem::take(&mut *self.0.lock().unwrap());
3395            for task in tasks {
3396                task.await.unwrap();
3397            }
3398        }
3399    }
3400
3401    #[tokio::test]
3402    async fn source_boxed_metadata_rows_list_successfully_without_mutating_storage() {
3403        use alibi_core::store::{CacheAdapter, MemoryCacheAdapter};
3404        let (ctx, owner, token) = context().await;
3405        let cache = Arc::new(MemoryCacheAdapter::new());
3406        let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3407            storage: ApiKeyStorageMode::SecondaryStorage,
3408            secondary_storage: Some(cache.clone()),
3409            ..Default::default()
3410        });
3411        let mut rows = Vec::new();
3412        let mut expected = std::collections::BTreeMap::new();
3413        // Actual pinned Source trusted creation admits boxed String("10"),
3414        // String("2"), Number(3), then serializes these primitive metadata
3415        // values. Their relational comparator is cyclic. Listing still
3416        // succeeds; no cross-engine total ordering is specified.
3417        for index in 0..64 {
3418            let created = server_key(&plugin, &ctx, &owner, "default").await;
3419            let id = format!("api-key:by-id:{}", created.api_key.id);
3420            let hash = format!("api-key:{}", ApiKeyPlugin::hash_key(&created.key));
3421            let mut row: serde_json::Value =
3422                serde_json::from_str(&cache.get(&id).await.unwrap().unwrap()).unwrap();
3423            *row.get_mut("metadata").unwrap() = match index % 6 {
3424                0 => json!("10"),
3425                1 => json!("2"),
3426                2 => json!(3),
3427                3 => json!(null),
3428                4 => json!("0"),
3429                _ => json!({"literal":"object"}),
3430            };
3431            let value = serde_json::to_string(&row).unwrap();
3432            cache.set_without_expiry(&id, &value).await.unwrap();
3433            cache.set_without_expiry(&hash, &value).await.unwrap();
3434            rows.push((id, value.clone()));
3435            rows.push((hash, value));
3436            expected.insert(
3437                created.api_key.id,
3438                [
3439                    json!(10),
3440                    json!(2),
3441                    json!(3),
3442                    json!(null),
3443                    json!(0),
3444                    json!({"literal":"object"}),
3445                ]
3446                .get(index % 6)
3447                .unwrap()
3448                .clone(),
3449            );
3450        }
3451        let reference = format!("api-key:by-ref:{owner}");
3452        let reference_before = cache.get(&reference).await.unwrap();
3453        for direction in ["asc", "desc"] {
3454            let mut req = request(&token, "/api-key/list", &json!(null));
3455            req.method = HttpMethod::Get;
3456            req.body = None;
3457            req.query = HashMap::from([
3458                ("sortBy".to_owned(), "metadata".to_owned()),
3459                ("sortDirection".to_owned(), direction.to_owned()),
3460            ]);
3461            let response = plugin.handle_list(&req, &ctx).await.unwrap();
3462            assert_eq!(response.status, 200);
3463            let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
3464            assert_eq!(body.get("total").unwrap(), 64);
3465            let actual: std::collections::BTreeMap<_, _> = body
3466                .get("apiKeys")
3467                .unwrap()
3468                .as_array()
3469                .unwrap()
3470                .iter()
3471                .map(|row| {
3472                    (
3473                        row.get("id").unwrap().as_str().unwrap().to_owned(),
3474                        row.get("metadata").unwrap().clone(),
3475                    )
3476                })
3477                .collect();
3478            assert_eq!(actual, expected);
3479            for (index, value) in &rows {
3480                assert_eq!(
3481                    cache.get(index).await.unwrap().as_deref(),
3482                    Some(value.as_str())
3483                );
3484            }
3485            assert_eq!(cache.get(&reference).await.unwrap(), reference_before);
3486            assert!(
3487                ctx.database
3488                    .list_api_keys_by_reference(&owner)
3489                    .await
3490                    .unwrap()
3491                    .is_empty()
3492            );
3493        }
3494        // Numeric metadata has an independent total ordering. Exercise a list
3495        // larger than 64 entries, then assert a page in both directions.
3496        for _ in 64..96 {
3497            let created = server_key(&plugin, &ctx, &owner, "default").await;
3498            let id = format!("api-key:by-id:{}", created.api_key.id);
3499            let hash = format!("api-key:{}", ApiKeyPlugin::hash_key(&created.key));
3500            let value = cache.get(&id).await.unwrap().unwrap();
3501            rows.push((id, value.clone()));
3502            rows.push((hash, value));
3503        }
3504        let mut ordered = std::collections::BTreeMap::new();
3505        for (index, pair) in rows.as_chunks_mut::<2>().0.iter_mut().enumerate() {
3506            let rank = (index * 37) % 96;
3507            let mut row: serde_json::Value =
3508                serde_json::from_str(&pair.first().unwrap().1).unwrap();
3509            _ = ordered.insert(rank, row.get("id").unwrap().clone());
3510            *row.get_mut("metadata").unwrap() = json!(rank);
3511            let serialized = serde_json::to_string(&row).unwrap();
3512            for (key, value) in pair {
3513                cache.set_without_expiry(key, &serialized).await.unwrap();
3514                *value = serialized.clone();
3515            }
3516        }
3517        let reference_before = cache.get(&reference).await.unwrap();
3518        for direction in ["asc", "desc"] {
3519            let mut req = request(&token, "/api-key/list", &json!(null));
3520            req.method = HttpMethod::Get;
3521            req.body = None;
3522            req.query = HashMap::from([
3523                ("sortBy".into(), "metadata".into()),
3524                ("sortDirection".into(), direction.into()),
3525                ("offset".into(), "11".into()),
3526                ("limit".into(), "9".into()),
3527            ]);
3528            let response = plugin.handle_list(&req, &ctx).await.unwrap();
3529            assert_eq!(response.status, 200);
3530            let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
3531            assert_eq!(body.get("total").unwrap(), 96);
3532            let page = body.get("apiKeys").unwrap().as_array().unwrap();
3533            assert_eq!(page.len(), 9);
3534            for (offset, row) in page.iter().enumerate() {
3535                let rank = if direction == "asc" {
3536                    11 + offset
3537                } else {
3538                    84 - offset
3539                };
3540                assert_eq!(row.get("id").unwrap(), ordered.get(&rank).unwrap());
3541                assert_eq!(row.get("metadata").unwrap(), &json!(rank));
3542            }
3543            for (key, value) in &rows {
3544                assert_eq!(
3545                    cache.get(key).await.unwrap().as_deref(),
3546                    Some(value.as_str())
3547                );
3548            }
3549            assert_eq!(cache.get(&reference).await.unwrap(), reference_before);
3550        }
3551    }
3552
3553    #[tokio::test]
3554    async fn application_storage_preserves_authority_indexes_and_usage() {
3555        use alibi_core::store::{CacheAdapter, MemoryCacheAdapter};
3556        // HTTP creation and trusted verification use the real plugin and stores;
3557        // independent database reads prove secondary-only keys create no SQL rows.
3558        for (fallback, custom, deferred) in [
3559            (false, false, false),
3560            (true, false, false),
3561            (false, true, false),
3562            (true, true, false),
3563            (false, true, true),
3564            (true, true, true),
3565        ] {
3566            let (mut ctx, owner, token) = context().await;
3567            let completions = Arc::new(ApplicationCompletions::default());
3568            Arc::make_mut(&mut ctx.config).background_tasks = Some(completions.clone());
3569            let outsider = ctx
3570                .database
3571                .create_user(
3572                    CreateUser::new()
3573                        .with_email("outsider@example.com")
3574                        .with_name("outsider"),
3575                )
3576                .await
3577                .unwrap();
3578            let cache = Arc::new(MemoryCacheAdapter::new());
3579            let decoy = Arc::new(MemoryCacheAdapter::new());
3580            let application = Arc::new(ApplicationKeyStorage {
3581                cache: cache.clone(),
3582                fail_writes: std::sync::atomic::AtomicBool::new(false),
3583            });
3584            let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3585                storage: ApiKeyStorageMode::SecondaryStorage,
3586                secondary_storage: Some(
3587                    if custom { decoy.clone() } else { cache.clone() } as Arc<dyn CacheAdapter>
3588                ),
3589                custom_storage: custom.then(|| application.clone() as Arc<dyn ApiKeyStorage>),
3590                fallback_to_database: fallback,
3591                defer_updates: deferred,
3592                rate_limit: RateLimitDefaults {
3593                    enabled: false,
3594                    ..Default::default()
3595                },
3596                ..Default::default()
3597            });
3598            let response = plugin
3599                .handle_create(
3600                    &request(&token, "/api-key/create", &json!({"name":"stored"})),
3601                    &ctx,
3602                )
3603                .await
3604                .unwrap();
3605            let created: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
3606            let id = created.get("id").unwrap().as_str().unwrap();
3607            let raw = created.get("key").unwrap().as_str().unwrap();
3608            plugin
3609                .update_key(
3610                    &ctx,
3611                    &UpdateKeyRequest {
3612                        key_id: id.to_owned(),
3613                        user_id: Some(owner.clone()),
3614                        remaining: Some(2.0),
3615                        ..Default::default()
3616                    },
3617                )
3618                .await
3619                .unwrap();
3620            let hash = ApiKeyPlugin::hash_key(raw);
3621            let by_id = format!("api-key:by-id:{id}");
3622            let by_hash = format!("api-key:{hash}");
3623            let by_ref = format!("api-key:by-ref:{owner}");
3624            let initial = cache.get(&by_id).await.unwrap().unwrap();
3625            assert_eq!(cache.get(&by_hash).await.unwrap(), Some(initial.clone()));
3626            assert_eq!(
3627                ctx.database.get_api_key_by_id(id).await.unwrap().is_some(),
3628                fallback
3629            );
3630            assert_eq!(cache.get(&by_ref).await.unwrap().is_none(), fallback);
3631            assert!(
3632                get_key_core(id, None, &outsider.id().to_string(), &plugin, &ctx)
3633                    .await
3634                    .is_err()
3635            );
3636            assert_eq!(cache.get(&by_id).await.unwrap(), Some(initial.clone()));
3637            assert!(
3638                plugin
3639                    .verify_api_key(
3640                        &VerifyApiKey {
3641                            key: raw,
3642                            config_id: Some("wrong"),
3643                            permissions: None
3644                        },
3645                        &ctx
3646                    )
3647                    .await
3648                    .is_err()
3649            );
3650            assert_eq!(cache.get(&by_id).await.unwrap(), Some(initial));
3651            let list = list_keys_core(&owner, &ListKeysQuery::default(), &plugin, &ctx)
3652                .await
3653                .unwrap();
3654            assert_eq!(list.total, 1);
3655            let input = VerifyApiKey {
3656                key: raw,
3657                config_id: None,
3658                permissions: None,
3659            };
3660            assert_eq!(
3661                plugin.verify_api_key(&input, &ctx).await.unwrap().remaining,
3662                Some(1.0)
3663            );
3664            completions.drain().await;
3665            assert!(decoy.get(&by_id).await.unwrap().is_none());
3666            let row: alibi_core::ApiKey =
3667                serde_json::from_str(&cache.get(&by_id).await.unwrap().unwrap()).unwrap();
3668            assert_eq!(row.remaining, Some(1.0));
3669            assert_eq!(row.reference_id, owner);
3670            assert_eq!(
3671                cache.get(&by_hash).await.unwrap(),
3672                cache.get(&by_id).await.unwrap()
3673            );
3674            if fallback {
3675                let persisted = ctx.database.get_api_key_by_id(id).await.unwrap().unwrap();
3676                let mut persisted_json = serde_json::to_value(&persisted).unwrap();
3677                // The database model stores JSON text; the public application
3678                // storage contract stores the actual metadata value.
3679                *persisted_json.get_mut("metadata").unwrap() =
3680                    serde_json::from_str(persisted.metadata.as_deref().unwrap_or("null")).unwrap();
3681                assert_eq!(persisted_json, serde_json::to_value(&row).unwrap());
3682                cache.delete(&by_id).await.unwrap();
3683                cache.delete(&by_hash).await.unwrap();
3684                assert_eq!(
3685                    get_key_core(id, None, &owner, &plugin, &ctx)
3686                        .await
3687                        .unwrap()
3688                        .remaining,
3689                    Some(1.0)
3690                );
3691                assert_eq!(
3692                    cache.get(&by_hash).await.unwrap(),
3693                    cache.get(&by_id).await.unwrap()
3694                );
3695            }
3696            assert_eq!(
3697                plugin.verify_api_key(&input, &ctx).await.unwrap().remaining,
3698                Some(0.0)
3699            );
3700            completions.drain().await;
3701            assert!(plugin.verify_api_key(&input, &ctx).await.is_err());
3702            completions.drain().await;
3703            assert!(cache.get(&by_id).await.unwrap().is_none());
3704            assert!(cache.get(&by_hash).await.unwrap().is_none());
3705            assert!(cache.get(&by_ref).await.unwrap().is_none());
3706            assert!(ctx.database.get_api_key_by_id(id).await.unwrap().is_none());
3707            if custom {
3708                let target = plugin
3709                    .create_key(
3710                        &ctx,
3711                        &CreateKeyRequest {
3712                            user_id: Some(owner.clone()),
3713                            remaining: Some(4.0),
3714                            ..Default::default()
3715                        },
3716                    )
3717                    .await
3718                    .unwrap();
3719                let raw = target.key.as_str();
3720                let key_id = &target.api_key.id;
3721                let id_index = format!("api-key:by-id:{key_id}");
3722                let hash_index = format!("api-key:{}", ApiKeyPlugin::hash_key(raw));
3723                let before = cache.get(&id_index).await.unwrap();
3724                application
3725                    .fail_writes
3726                    .store(true, std::sync::atomic::Ordering::SeqCst);
3727                let input = VerifyApiKey {
3728                    key: raw,
3729                    config_id: None,
3730                    permissions: None,
3731                };
3732                let failed = plugin.verify_api_key(&input, &ctx).await;
3733                assert_eq!(failed.is_ok(), deferred && !fallback);
3734                completions.drain().await;
3735                assert_eq!(cache.get(&id_index).await.unwrap(), before);
3736                assert_eq!(cache.get(&hash_index).await.unwrap(), before);
3737                if fallback {
3738                    assert_eq!(
3739                        ctx.database
3740                            .get_api_key_by_id(key_id)
3741                            .await
3742                            .unwrap()
3743                            .unwrap()
3744                            .remaining,
3745                        Some(3.0)
3746                    );
3747                } else {
3748                    assert!(
3749                        ctx.database
3750                            .get_api_key_by_id(key_id)
3751                            .await
3752                            .unwrap()
3753                            .is_none()
3754                    );
3755                }
3756                application
3757                    .fail_writes
3758                    .store(false, std::sync::atomic::Ordering::SeqCst);
3759                let retried = plugin.verify_api_key(&input, &ctx).await.unwrap();
3760                assert_eq!(retried.remaining, Some(if fallback { 2.0 } else { 3.0 }));
3761                completions.drain().await;
3762                assert_eq!(
3763                    cache.get(&hash_index).await.unwrap(),
3764                    cache.get(&id_index).await.unwrap()
3765                );
3766                let row: alibi_core::ApiKey =
3767                    serde_json::from_str(&cache.get(&id_index).await.unwrap().unwrap()).unwrap();
3768                assert_eq!(row.remaining, retried.remaining);
3769            }
3770        }
3771        // Secondary-only rate limiting owns its own update path. A denied
3772        // request must not spend quota, and an elapsed window must refill once.
3773        let (ctx, owner, _) = context().await;
3774        let cache = Arc::new(MemoryCacheAdapter::new());
3775        let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3776            storage: ApiKeyStorageMode::SecondaryStorage,
3777            secondary_storage: Some(cache.clone()),
3778            ..Default::default()
3779        });
3780        let created = plugin
3781            .create_key(
3782                &ctx,
3783                &CreateKeyRequest {
3784                    user_id: Some(owner.clone()),
3785                    remaining: Some(2.0),
3786                    rate_limit_enabled: Some(true),
3787                    rate_limit_time_window: Some(60_000.0),
3788                    rate_limit_max: Some(1.0),
3789                    refill_interval: Some(60_000.0),
3790                    refill_amount: Some(5.0),
3791                    ..Default::default()
3792                },
3793            )
3794            .await
3795            .unwrap();
3796        let by_id = format!("api-key:by-id:{}", created.api_key.id);
3797        let by_hash = format!("api-key:{}", ApiKeyPlugin::hash_key(&created.key));
3798        let input = VerifyApiKey {
3799            key: &created.key,
3800            config_id: None,
3801            permissions: None,
3802        };
3803        let first = plugin.verify_api_key(&input, &ctx).await.unwrap();
3804        assert_eq!(first.remaining, Some(1.0));
3805        assert_eq!(first.request_count, Some(1.0));
3806        let before = cache.get(&by_id).await.unwrap();
3807        assert_eq!(cache.get(&by_hash).await.unwrap(), before);
3808        let denied = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
3809        let ApiKeyVerificationError::Validation(denied) = denied else {
3810            panic!("Expected rate-limit validation");
3811        };
3812        assert_eq!(denied.code, ApiKeyErrorCode::RateLimited);
3813        assert!(denied.details.unwrap().try_again_in > 0.0);
3814        assert_eq!(cache.get(&by_id).await.unwrap(), before);
3815        assert_eq!(cache.get(&by_hash).await.unwrap(), before);
3816        let mut elapsed: alibi_core::ApiKey =
3817            serde_json::from_str(before.as_deref().unwrap()).unwrap();
3818        let previous = (chrono::Utc::now() - chrono::Duration::minutes(2)).to_rfc3339();
3819        elapsed.last_request = Some(previous.clone());
3820        elapsed.last_refill_at = Some(previous.clone());
3821        let seeded = serde_json::to_string(&elapsed).unwrap();
3822        cache.set_without_expiry(&by_id, &seeded).await.unwrap();
3823        cache.set_without_expiry(&by_hash, &seeded).await.unwrap();
3824        let refilled = plugin.verify_api_key(&input, &ctx).await.unwrap();
3825        assert_eq!(refilled.remaining, Some(4.0));
3826        assert_eq!(refilled.request_count, Some(1.0));
3827        assert_ne!(refilled.last_refill_at.as_deref(), Some(previous.as_str()));
3828        let observed: alibi_core::ApiKey =
3829            serde_json::from_str(&cache.get(&by_id).await.unwrap().unwrap()).unwrap();
3830        assert_eq!(observed.remaining, Some(4.0));
3831        assert_eq!(observed.request_count, Some(1.0));
3832        assert_eq!(
3833            cache.get(&by_id).await.unwrap(),
3834            cache.get(&by_hash).await.unwrap()
3835        );
3836        assert!(
3837            ctx.database
3838                .list_api_keys_by_reference(&owner)
3839                .await
3840                .unwrap()
3841                .is_empty()
3842        );
3843    }
3844}
3845// LCOV_EXCL_STOP