eve_esi 0.4.9

Thread-safe, asynchronous client for EVE Online's ESI & OAuth2
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
//! # EVE Online ESI Client Config
//!
//! Provides methods to override defaults for the [`Client`](crate::Client). This allows the
//! modification of the base ESI URL, OAuth2 endpoint URLs and the logic of how JWT
//! key caching and refreshing is handled.
//!
//! ## Features
//! - Override the base ESI URL
//! - Override EVE Online OAuth2 authorization, JWT key, and token endpoint URLs
//! - Adjust expiration time & threshold for a proactive refresh for the JWT key cache used to validate tokens
//! - Adjust the timeout between sets of JWT key refresh attempts
//! - Adjust backoff period (wait time) beteween attempts and how many retries should be made to refresh JWT keys
//! - Enable/disable the proactive background JWT key refresh
//!
//! ## Builder Methods
//!
//! | Method                              | Description                                                |
//! | ----------------------------------- | ---------------------------------------------------------- |
//! | `new`                               | Create a new [`ConfigBuilder`]                             |
//! | `build`                             | Build the [`Config`]                                       |
//! | `esi_url`                           | Base URL for ESI endpoints                                 |
//! | `auth_url`                          | URL for sign in with EVE Online                            |
//! | `token_url`                         | URL to retrieve access tokens for OAuth2                   |
//! | `jwk_url`                           | URL for JWT keys to validate tokens                        |
//! | `jwk_cache_ttl`                     | The time that JWT keys are cached for                      |
//! | `jwk_refresh_backoff`               | How long to wait between retries                           |
//! | `jwk_refresh_timeout`               | How long to wait for another thread to refresh             |
//! | `jwk_refresh_cooldown`              | Cooldown between sets of JWT key refresh attempts          |
//! | `jwk_refresh_max_retries`           | Amount of retries when a key fetch fails                   |
//! | `jwk_background_refresh_enabled`    | Enable/disable background refresh                          |
//! | `jwk_background_refresh_threshold`  | Percentage at which cache is refreshed proactively         |
//! | `jwt_issuers`                       | Expected issuer(s) of JWT tokens                           |
//! | `jwt_audience`                      | Intended audience JWT tokens are to be used with           |
//! | `esi_validate_token_before_request` | Toggle validating tokens before authenticated ESI requests |
//!
//! ## Usage
//!
//! ```
//! use std::time::Duration;
//!
//! // Build a config to override defaults
//! let config = eve_esi::Config::builder()
//!     // Set JWT key cache lifetime to 7200 seconds representing 2 hours
//!     .jwk_cache_ttl(Duration::from_secs(7200))
//!     .build()
//!     .expect("Failed to build ESI Config");
//!
//! // Always set a user_agent to identify your application when making requests
//! let user_agent = "MyApp/1.0 (contact@example.com; +https://github.com/your/repository)";
//!
//! // Apply config settings to Client
//! let esi_client = eve_esi::Client::builder()
//!     .config(config)
//!     .user_agent(user_agent)
//!     .build()
//!     .expect("Failed to build ESI Client");
//! ```

use std::time::Duration;

use oauth2::{AuthUrl, TokenUrl};

use crate::{
    constant::{
        DEFAULT_AUTH_URL, DEFAULT_ESI_URL, DEFAULT_JWT_AUDIENCE, DEFAULT_JWT_ISSUERS,
        DEFAULT_TOKEN_URL,
    },
    error::{ConfigError, Error},
    oauth2::jwk::cache::JwtKeyCacheConfig,
};

/// Configuration settings for the [`Client`](crate::Client)
///
/// For a full overview, features, and usage examples, see the [module-level documentation](self).
pub struct Config {
    // URL settings
    /// The base EVE Online ESI API URL
    pub(crate) esi_url: String,
    /// Authorization URL used to login with EVE Online's OAuth2
    pub(crate) auth_url: AuthUrl,
    /// Token URL which provides an access token for authenticated ESI endpoints
    pub(crate) token_url: TokenUrl,

    // JWT Key Settings
    /// Config for JWT key caching & refreshing
    pub(crate) jwt_key_cache_config: JwtKeyCacheConfig,
    /// The EVE Online login server which represents the expected issuer of tokens
    pub(crate) jwt_issuers: Vec<String>,
    /// The intended audience which JWT tokens will be used with
    pub(crate) jwt_audience: String,

    // ESI Request Settings
    /// Enable/disable checking if access token is valid, not expired, and has required scopes before an ESI request
    pub(crate) esi_validate_token_before_request: bool,
}

/// Builder struct for configuring & constructing an [`Config`] to override default [`Client`](crate::Client) settings
///
/// For a full overview, features, and usage examples, see the [module-level documentation](self).
pub struct ConfigBuilder {
    // URL settings
    /// The base EVE Online ESI API URL
    pub(crate) esi_url: String,
    /// Authorization URL used to login with EVE Online's OAuth2
    pub(crate) auth_url: String,
    /// Token URL which provides an access token for authenticated ESI endpoints
    pub(crate) token_url: String,

    // OAuth2 JWT key config
    /// Config for OAuth2 JWT key caching & refreshing
    pub(crate) jwt_key_cache_config: JwtKeyCacheConfig,
    /// The EVE Online login server URL which represents the expected issuer of tokens
    pub(crate) jwt_issuers: Vec<String>,
    /// The intended audience which JWT tokens will be used with
    pub(crate) jwt_audience: String,

    // ESI Request Settings
    /// Enable/disable checking if access token is valid, not expired, and has required scopes before an ESI request
    pub(crate) esi_validate_token_before_request: bool,
}

impl Config {
    /// Creates a new instance of [`Config`] with default settings
    ///
    /// For details see [module-level documentation](self).
    ///
    /// # Returns
    /// - [`Config`]: With the default configuration
    ///
    /// # Errors
    /// - [`Error`]: If the default [`ConfigBuilder::jwk_background_refresh_threshold`] is configured incorrectly.
    pub fn new() -> Result<Self, Error> {
        ConfigBuilder::new().build()
    }

    /// Returns a [`ConfigBuilder`] instance used to configure JWT key related settings
    ///
    /// Allows for the configuration of the [`Config`] using the [`ConfigBuilder`]
    /// setter methods to override the default configuration.
    ///
    /// For details see [module-level documentation](self).
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the default config which can be overridden with setter methods.
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::new()
    }
}

impl Default for ConfigBuilder {
    /// Create a default instance of [`ConfigBuilder`]
    fn default() -> Self {
        Self::new()
    }
}

impl ConfigBuilder {
    /// Creates a new [`ConfigBuilder`] instance used to build an [`Config`]
    ///
    /// For a full overview, features, and usage example, see the [module-level documentation](self).
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the default settings that can be modified with the setter methods.
    pub fn new() -> Self {
        // Convert default JWT issuers into Vec<String>
        let issuers: Vec<String> = DEFAULT_JWT_ISSUERS
            .to_vec()
            .iter()
            .map(|str| str.to_string())
            .collect();

        Self {
            // URL settings
            esi_url: DEFAULT_ESI_URL.to_string(),
            auth_url: DEFAULT_AUTH_URL.to_string(),
            token_url: DEFAULT_TOKEN_URL.to_string(),

            // OAuth2 JWT key config
            jwt_key_cache_config: JwtKeyCacheConfig::new(),
            jwt_issuers: issuers,
            jwt_audience: DEFAULT_JWT_AUDIENCE.to_string(),

            // ESI Request Settings
            esi_validate_token_before_request: true,
        }
    }

    /// Builds a [`Config`] instance
    ///
    /// Converts an [`ConfigBuilder`] into a [`Config`] with the configured values that
    /// were set with the builder methods.
    ///
    /// For a full overview, features, and usage example, see the [module-level documentation](self).
    ///
    /// # Returns
    /// - [`Config`]: instance with the settings configured on the builder
    ///
    /// # Errors
    /// Returns an [`Error`] if one of the following occurs:
    /// - The [`Self::jwk_background_refresh_threshold`] method is given a value less than 1 or over 99
    /// - The [`Self::auth_url`] method is given an invalid URL
    /// - The [`Self::token_url`] method is given an invalid URL
    pub fn build(self) -> Result<Config, Error> {
        // Ensure background refresh percentage is set properly
        if self.jwt_key_cache_config.background_refresh_threshold == 0 {
            return Err(Error::ConfigError(
                ConfigError::InvalidBackgroundRefreshThreshold,
            ));
        }
        if self.jwt_key_cache_config.background_refresh_threshold >= 100 {
            return Err(Error::ConfigError(
                ConfigError::InvalidBackgroundRefreshThreshold,
            ));
        }

        // Parse URLs
        let auth_url = match AuthUrl::new(self.auth_url.clone()) {
            Ok(url) => url,
            Err(_) => return Err(Error::ConfigError(ConfigError::InvalidAuthUrl)),
        };
        let token_url = match TokenUrl::new(self.token_url.clone()) {
            Ok(url) => url,
            Err(_) => {
                return Err(Error::ConfigError(ConfigError::InvalidTokenUrl));
            }
        };

        Ok(Config {
            // URL settings
            esi_url: self.esi_url,
            auth_url,
            token_url,

            // JWT key cache settings
            jwt_key_cache_config: self.jwt_key_cache_config,
            jwt_issuers: self.jwt_issuers,
            jwt_audience: self.jwt_audience,

            // ESI Request Settings
            esi_validate_token_before_request: self.esi_validate_token_before_request,
        })
    }

    /// Sets the EVE Online ESI base URL
    ///
    /// This method configures the base URL for EVE Online ESI.
    /// This is generally used for tests using a mock server with crates such as
    /// [mockito](https://crates.io/crates/mockito) to avoid actual ESI API calls.
    ///
    /// # Arguments
    /// - `esi_url` (&[`str`]): The EVE Online ESI API base URL.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the updated ESI URL
    pub fn esi_url(mut self, esi_url: &str) -> Self {
        self.esi_url = esi_url.to_string();
        self
    }

    /// Sets the EVE Online OAuth2 authorizion URL
    ///
    /// This method configures the authorize URL for EVE Online oauth2.
    /// This is generally used for tests using a mock server with crates such as
    /// [mockito](https://crates.io/crates/mockito) to avoid actual ESI API calls.
    ///
    /// # Arguments
    /// - `auth_url` (&[`str`]): The EVE Online OAuth2 authorization endpoint URL.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with updated EVE Online OAuth2 authorization URL.
    pub fn auth_url(mut self, auth_url: &str) -> Self {
        self.auth_url = auth_url.to_string();
        self
    }

    /// Sets the EVE Online OAuth2 token URL
    ///
    /// This method configures the token URL for EVE Online oauth2 to a custom URL.
    /// This is generally used for tests using a mock server with crates such as
    /// [mockito](https://crates.io/crates/mockito) to avoid actual ESI API calls.
    ///
    /// # Arguments
    /// - `token_url` (&[`str`]): The EVE Online OAuth2 token endpoint URL.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with updated EVE Online OAuth2 token URL.
    pub fn token_url(mut self, token_url: &str) -> Self {
        self.token_url = token_url.to_string();
        self
    }

    /// Sets the EVE Online JWT key URL used to fetch keys to validate tokens.
    ///
    /// This method configures the JWT key URL for EVE Online OAuth2 to a custom URL.
    /// This is generally used for tests using a mock server with crates such as
    /// [mockito](https://crates.io/crates/mockito) to avoid actual ESI API calls.
    ///
    /// # Arguments
    /// - `jwk_url` (&[`str`]): The EVE Online JWK endpoint URL.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with updated EVE Online JWK URL configuration.
    pub fn jwk_url(mut self, jwk_url: &str) -> Self {
        self.jwt_key_cache_config.jwk_url = jwk_url.to_string();
        self
    }

    /// Modifies the default lifetime of the JWT keys stored in cache
    ///
    /// By default, JWT keys are stored in cache for 3600 seconds (1 hour)
    /// before they are considered expired and need to be refreshed.
    ///
    /// Additionally, JWT keys are proactively refreshed by a background
    /// task at 80% expiration. You may wish to modify it with the
    /// [`Self::jwk_background_refresh_threshold`] method or disable the
    /// background refresh altogether with [`Self::jwk_background_refresh_enabled`]
    /// method.
    ///
    /// # Arguments
    /// - `duration` ([`Duration`]): The lifetime of the JWT keys stored in the cache.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the updated JWT key cache TTL
    pub fn jwk_cache_ttl(mut self, duration: Duration) -> Self {
        self.jwt_key_cache_config.cache_ttl = duration;
        self
    }

    /// Modifies the exponential backoff duration in milliseconds between JWT key fetch retry attempts
    ///
    /// The default behavior is a 100ms exponential backoff between each retry attempt
    /// to refresh JWT keys when the cache is either empty or expired.
    ///
    /// For example: 100ms, 200ms, 400ms, etc
    ///
    /// The amount of retry attempts can be modified with the [`Self::jwk_refresh_max_retries`]
    /// method.
    ///
    /// This does not affect the background JWT key refresh as it only makes one attempt with
    /// a 60 second cooldown between each attempt which can be modified with the
    /// [`Self::jwk_refresh_cooldown`] method.
    ///
    /// # Arguments
    /// - `duration` ([`Duration`]): The exponential backoff duration between each attempt.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the updated exponential backoff
    pub fn jwk_refresh_backoff(mut self, duration: Duration) -> Self {
        self.jwt_key_cache_config.refresh_backoff = duration;
        self
    }

    /// Modifies the timeout waiting for another thread to perform a JWT key cache refresh
    ///
    /// This library uses a refresh lock shared between threads to indicate if a JWT key
    /// cache refresh is already in progress on another thread. If the JWT key cache is
    /// currently empty or expired and this refresh lock is in place, the current thread
    /// will wait for a default of 5 seconds before timing out if the refresh takes too long.
    ///
    /// # Arguments
    /// - `duration` ([`Duration`]): Timeout duration to wait for another thread to complete a
    ///   JWT key refresh.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the modified timeout setting.
    pub fn jwk_refresh_timeout(mut self, duration: Duration) -> Self {
        self.jwt_key_cache_config.refresh_timeout = duration;
        self
    }

    /// Modifies the cooldown between sets of JWT key cache refresh attempts in the event of failure
    ///
    /// By default, when a set of JWT key cache refresh attempts fail there will be a cooldown of 60 seconds
    /// between the next set of attempts to refresh JWT keys before expiration.
    ///
    /// # Arguments
    /// - `duration` ([`Duration`]): Cooldown duration between background JWT key cache refresh attempts.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the modified background refresh cooldown.
    pub fn jwk_refresh_cooldown(mut self, duration: Duration) -> Self {
        self.jwt_key_cache_config.refresh_cooldown = duration;
        self
    }

    /// Modifies the max amount of refresh attempts when fetching JWT keys
    ///
    /// This determines how many attempts are made to refresh JWT keys when
    /// the cache is empty or fully expired and it is imperative to refresh the
    /// cache in order to validate tokens.
    ///
    /// Between each fetch attempt there is an exponential backoff of 100ms by default
    /// which can be modified with the [`Self::jwk_refresh_backoff`] method.
    ///
    /// This does not affect the background JWT key refresh as it only makes one attempt with
    /// a 60 second cooldown between each attempt which can be modified with the
    /// [`Self::jwk_refresh_cooldown`] method.
    ///
    /// # Arguments
    /// - `retry_attempts` ([`u32`]): The amount of retry attempts if a JWT key fetch fails.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the updated JWK refresh max retries
    pub fn jwk_refresh_max_retries(mut self, retry_attempts: u32) -> Self {
        self.jwt_key_cache_config.refresh_max_retries = retry_attempts;
        self
    }

    /// Modifies whether or not the proactive background refresh when JWT keys are almost expired is enabled
    ///
    /// By default, when the JWT key cache is nearing expiration at around 80%, a background refresh task
    /// will be spawned to proactively refresh the keys. This behavior is thread safe and a more detailed
    /// description of how it works can be found at [`crate::oauth2::jwk::JwkApi::get_jwt_keys`].
    ///
    /// This functionality has been built with high volume applications in mind and will work for the
    /// vast majority of production use cases. In the instance where you do want to have more control
    /// over proactive JWT key refreshes consider disabling this and using a cron task to perform
    /// a refresh instead with the [`crate::oauth2::jwk::JwkApi::fetch_and_update_cache`] method which will
    /// update the cache regardless of expiration status.
    ///
    /// You can modify the % at which the proactive background refresh is triggered with the
    /// [`Self::jwk_background_refresh_threshold`] method.
    ///
    /// # Arguments
    /// - `background_refresh_enabled` ([`bool`]): A bool indicating whether or not the background refresh
    ///   is enabled.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the background refresh is enabled or disabled.
    pub fn jwk_background_refresh_enabled(mut self, background_refresh_enabled: bool) -> Self {
        self.jwt_key_cache_config.background_refresh_enabled = background_refresh_enabled;
        self
    }

    /// The % of JWT key cache lifetime for when the proactive background JWT key cache refresh is triggered
    ///
    /// By default, when the JWT key cache reaches 80% of the default 3600 second cache lifetime, a proactive
    /// JWT key cache refresh will be triggered next time the [`crate::oauth2::jwk::JwkApi::get_jwt_keys`]
    /// method is called.
    ///
    /// Should the attempt fail, there will be a 60 second cooldown between attempts which can be modified with
    /// the [`Self::jwk_refresh_cooldown`] method.
    ///
    /// # Arguments
    /// - `threshold_percent` ([`u64`]): A number representing the percentage of when the refresh should be triggered.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with the modified proactive background refresh threshold percentage.
    pub fn jwk_background_refresh_threshold(mut self, threshold_percentage: u64) -> Self {
        self.jwt_key_cache_config.background_refresh_threshold = threshold_percentage;
        self
    }

    /// Expected issuer(s) of JWT tokens
    ///
    /// This is the expected issuer(s) of JSON web tokens used to access
    /// authenticated ESI routes. This would be the EVE Online login server URL.
    /// At least 1 of the issuers provided must be present for token validation to be
    /// successful.
    ///
    /// # Arguments
    /// - `issuers` (`Vec<String>`): The expected issuer(s) of the JWT token.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with updated EVE Online login URL.
    pub fn jwt_issuers(mut self, issuers: Vec<String>) -> Self {
        self.jwt_issuers = issuers;
        self
    }

    /// Intended audience JWT tokens are to be used with
    ///
    /// The intended audience which the JSON web tokens (JWTs) used to access authenticated
    /// ESI routes will be used with. This is primarily used for JWT validation.
    ///
    /// # Arguments
    /// - `audience` (&[`str`]): The audience which tokens will be used with.
    ///   Default is `"EVE Online"`.
    ///
    /// # Returns
    /// - [`ConfigBuilder`]: Instance with updated JWT audience.
    pub fn jwt_audience(mut self, audience: &str) -> Self {
        self.jwt_audience = audience.to_string();
        self
    }

    /// Enable/disable checking if access token is valid, not expired, and has required scopes before an ESI request
    ///
    /// Enabled by default, disable this if you would prefer to do the checks manually or not at all. Please see
    /// the ESI Error Rates Limits section of [`crate::endpoints`] module documentation for the implications of
    /// disabling this.
    ///
    /// # Arguments
    /// - `enabled` (`bool`): indicates whether or not access tokens are validated prior to authenticated ESI route
    ///   requests.
    pub fn esi_validate_token_before_request(mut self, enabled: bool) -> Self {
        self.esi_validate_token_before_request = enabled;
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Ensures that all setter methods for [`ConfigBuilder`] work as expected
    ///
    /// Test Setup
    /// - Create a new instance of [`ConfigBuilder`] and use each setter method
    /// - Build the [`ConfigBuilder`] returning an [`Config`]
    ///
    /// Assertions
    /// - Assert URL settings were set as expected
    /// - Assert JWT key settings were set as expected
    /// - Assert JWT key background refresh settings were set as expected
    #[test]
    fn test_default_config_setter_methods() {
        let zero_seconds = Duration::from_secs(0);

        let config = ConfigBuilder::default()
            // URL settings
            .auth_url("https://example.com")
            .token_url("https://example.com")
            .jwk_url("https://example.com")
            // JWT key settings
            .jwk_cache_ttl(zero_seconds)
            .jwk_refresh_backoff(zero_seconds)
            .jwk_refresh_timeout(zero_seconds)
            .jwk_refresh_cooldown(zero_seconds)
            .jwk_refresh_max_retries(0)
            // Background refresh settings
            .jwk_background_refresh_enabled(false)
            .jwk_background_refresh_threshold(1)
            // JWT settings
            .jwt_issuers(vec!["example".to_string()])
            .jwt_audience("example")
            // ESI Request Settings
            .esi_validate_token_before_request(false)
            .build()
            .expect("Failed to build Config");

        // Assert URL settings were set
        let auth_url = AuthUrl::new("https://example.com".to_string()).unwrap();
        let token_url = TokenUrl::new("https://example.com".to_string()).unwrap();

        assert_eq!(config.auth_url, auth_url);
        assert_eq!(config.token_url, token_url);
        assert_eq!(config.jwt_key_cache_config.jwk_url, "https://example.com");

        // Assert JWT key settings were set
        assert_eq!(config.jwt_key_cache_config.cache_ttl, zero_seconds);
        assert_eq!(config.jwt_key_cache_config.refresh_backoff, zero_seconds);
        assert_eq!(config.jwt_key_cache_config.refresh_timeout, zero_seconds);
        assert_eq!(config.jwt_key_cache_config.refresh_cooldown, zero_seconds);
        assert_eq!(config.jwt_key_cache_config.refresh_max_retries, 0);

        // Assert JWT key background refresh settings were set
        assert_eq!(
            config.jwt_key_cache_config.background_refresh_enabled,
            false
        );
        assert_eq!(config.jwt_key_cache_config.background_refresh_threshold, 1);

        // Assert JWT settings were set
        assert_eq!(config.jwt_issuers, vec!["example"]);
        assert_eq!(config.jwt_audience, "example");

        // Assert ESI request settings was set
        assert_eq!(config.esi_validate_token_before_request, false)
    }

    /// Expect an error setting the JWK background refresh threshold to 0
    ///
    /// # Test Setup
    /// - Attempt to build a [`Config`] with the jwk_background_refresh_threshold to 0
    ///
    /// # Assertions
    /// - Assert result is an error
    /// - Assert error is of type [`OAuthConfigError::InvalidBackgroundRefreshThreshold`]
    #[test]
    fn test_invalid_background_refresh_threshold_0() {
        // Create a Config with invalid threshold percent
        let result = Config::builder()
            .jwk_background_refresh_threshold(0)
            .build();

        // Assert result is error
        assert!(result.is_err());

        // Assert error is of type OAuthConfigError::InvalidBackgroundRefreshThreshold
        assert!(matches!(
            result,
            Err(Error::ConfigError(
                ConfigError::InvalidBackgroundRefreshThreshold
            ))
        ))
    }

    /// Expect an error setting the JWK background refresh threshold to 100
    ///
    /// # Test Setup
    /// - Attempt to build an [`Config`] with the jwk_background_refresh_threshold to 100
    ///
    /// # Assertions
    /// - Assert result is an error
    /// - Assert error is of type [`ConfigError::InvalidBackgroundRefreshThreshold`]
    #[test]
    fn test_invalid_background_refresh_threshold_100() {
        // Create a Config with invalid threshold percent
        let result = Config::builder()
            .jwk_background_refresh_threshold(100)
            .build();

        // Assert result is error
        assert!(result.is_err());

        // Assert error is of type ConfigError::InvalidBackgroundRefreshThreshold
        assert!(matches!(
            result,
            Err(Error::ConfigError(
                ConfigError::InvalidBackgroundRefreshThreshold
            ))
        ))
    }

    /// Tests the attempting initialize an Config with an invalid auth_url
    ///
    /// # Test Setup
    /// - Create a Config with an invalid auth_url
    ///
    /// # Assertions
    /// - Assert result is an Error
    /// - Assert error is of the ConfigError:InvalidAuthUrl variant
    #[test]
    fn test_invalid_auth_url() {
        // Create a Config with an invalid auth_url
        let result = Config::builder().auth_url("invalid_url").build();

        // Assert result is an Error
        assert!(result.is_err());

        // Assert error is of the ConfigError:InvalidAuthUrl variant
        assert!(matches!(
            result,
            Err(Error::ConfigError(ConfigError::InvalidAuthUrl))
        ));
    }

    /// Tests the attempting initialize an Config with an invalid token_url
    ///
    /// # Test Setup
    /// - Create a Config with an invalid token_url
    ///
    /// # Assertions
    /// - Assert result is an Error
    /// - Assert error is of the ConfigError:InvalidTokenUrl variant
    #[test]
    fn test_invalid_token_url() {
        // Create a Config with an invalid token_url
        let result = Config::builder().token_url("invalid_url").build();

        // Assert result is an Error
        assert!(result.is_err());

        // Assert error is of the ConfigError:InvalidTokenUrl variant
        assert!(matches!(
            result,
            Err(Error::ConfigError(ConfigError::InvalidTokenUrl))
        ));
    }
}