jar_support.rs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. //! Shared scaffolding for the `test(jar)` suites.
  2. //!
  3. //! Fixture client projection, JWKS, shared Redis handles, and outcome
  4. //! helpers. Included via `#[path]` from `jar.rs` and `jar_request_uri.rs`;
  5. //! this file itself carries no tests.
  6. //!
  7. //! Redis comes from the shared services in `compose.test.yaml`
  8. //! (`docker compose -f compose.test.yaml up -d`); each `store_conn` call
  9. //! uses unique key prefixes, so parallel tests never share state (and never
  10. //! FLUSH shared state).
  11. use std::time::Duration;
  12. use d_oauth::{
  13. db::client_repo::Client,
  14. error::{AppError, OAuthErrorCode},
  15. state::RedisConn,
  16. store::Store,
  17. };
  18. use serde_json::{Value, json};
  19. #[path = "common/mod.rs"]
  20. mod common;
  21. use common::{redis_url, unique_prefixes};
  22. #[path = "../src/test_guard.rs"]
  23. #[allow(dead_code)]
  24. mod test_guard;
  25. use test_guard::{Lease, Shared};
  26. use time::OffsetDateTime;
  27. /// Test body result (no `unwrap`/`expect` under the deny lints).
  28. pub type TestResult = Result<(), TestError>;
  29. /// Errors raised by the JAR test scaffolding itself.
  30. #[derive(Debug, thiserror::Error)]
  31. pub enum TestError {
  32. /// The shared test service is unreachable (usually: compose not started).
  33. #[error("test setup: {0}")]
  34. Setup(String),
  35. /// A Redis command failed.
  36. #[error("redis error: {0}")]
  37. Redis(#[from] redis::RedisError),
  38. /// A store operation failed.
  39. #[error("store error: {0}")]
  40. Store(#[from] d_oauth::store::StoreError),
  41. /// A JWT operation failed.
  42. #[error("jwt error: {0}")]
  43. Jwt(#[from] jsonwebtoken::errors::Error),
  44. /// A JSON operation failed.
  45. #[error("json error: {0}")]
  46. Json(#[from] serde_json::Error),
  47. /// A test assertion observed an unexpected value.
  48. #[error("unexpected test value: {0}")]
  49. Unexpected(String),
  50. }
  51. /// Test client id used across the suite.
  52. pub const CLIENT_ID: &str = "jar-test-client";
  53. /// Test server issuer (the required request-object `aud`).
  54. pub const ISSUER: &str = "https://auth.example.test";
  55. /// `kid` of the client's registered JAR signing key.
  56. pub const KEY_ID: &str = "jar-test-key";
  57. /// Modulus of the client's RSA key (base64url, no pad).
  58. pub const CLIENT_JWK_N: &str = "s8FfX2aG2tOjxpTathl42eHWWkAwCxes1AqepAwEalfvpCeN3O7urlCzzsHqKTXohpNe0fMSpNQjMjG75GzMi8DpjdGb8SqvnR9UP21-peToq0duo8fBvrxbBrLkc_8zlQeT4EN0RIcT-kJ8Uu68AXA9kCxjt2wWVy7n6V06YhfiCCsIknc43TunHE2VQAQovC0KxDQbfxsZhrQPLuc-OEoq2aIaGDrty2G0TqdtU7aXzmYDHMgmDlG_oPQNAzVjHNl5--uOUT3UWEF1gF1Bvsvp2FqsXrzgeb_UmlGBZp8adt6GM1Ma14EoSYTEwMYYCLYZmk9L56ykT0RrykHEYQ";
  59. /// The client's registered JWKS document (single RS256 key).
  60. pub fn client_jwks() -> Value {
  61. json!({"keys": [{
  62. "kty": "RSA", "n": CLIENT_JWK_N, "e": "AQAB",
  63. "kid": KEY_ID, "alg": "RS256", "use": "sig",
  64. }]})
  65. }
  66. /// A registered client projection for the suite.
  67. pub fn test_client(auth_method: &str, request_uris: Option<Value>) -> Client {
  68. let now = OffsetDateTime::now_utc();
  69. Client {
  70. id: uuid::Uuid::new_v4(),
  71. client_id: CLIENT_ID.to_owned(),
  72. client_secret_expires_at: None,
  73. client_id_issued_at: now,
  74. client_name: "jar test client".to_owned(),
  75. grant_types: vec!["authorization_code".to_owned()],
  76. response_types: vec!["code".to_owned()],
  77. scope: "read write".to_owned(),
  78. token_endpoint_auth_method: auth_method.to_owned(),
  79. jwks: Some(client_jwks()),
  80. jwks_uri: None,
  81. tls_client_auth_subject_dn: None,
  82. tls_client_auth_cert_thumbprint: None,
  83. request_uris,
  84. contacts: None,
  85. software_id: None,
  86. dpop_bound_access_tokens: false,
  87. access_token_format: "opaque".to_owned(),
  88. active: true,
  89. created_at: now,
  90. updated_at: now,
  91. }
  92. }
  93. /// Shared Redis server handle (no per-test container: the URL points at the
  94. /// shared services from `compose.test.yaml`).
  95. ///
  96. /// The lease keeps the shared slot alive; bind it (usually as `_redis`) for
  97. /// the duration of the test.
  98. pub struct JarRedis {
  99. url: String,
  100. }
  101. static JAR_REDIS: Shared<JarRedis> = Shared::new();
  102. /// Resolve the shared Redis URL once; each test connects its own manager.
  103. ///
  104. /// The returned lease keeps the shared slot alive; bind it (usually as
  105. /// `_redis`) for the duration of the test.
  106. async fn shared_redis() -> Result<Lease<JarRedis>, TestError> {
  107. JAR_REDIS
  108. .lease::<TestError>(async { Ok(JarRedis { url: redis_url() }) })
  109. .await
  110. }
  111. /// A connected [`RedisConn`] plus [`Store`] over the shared Redis.
  112. ///
  113. /// The returned lease keeps the shared slot alive; bind it (usually as
  114. /// `_redis`) for the duration of the test. Key prefixes are unique per
  115. /// call, so parallel tests never share state.
  116. pub async fn store_conn() -> Result<(Lease<JarRedis>, RedisConn, Store), TestError> {
  117. let redis = shared_redis().await?;
  118. let client = redis::Client::open(redis.url.as_str()).map_err(|error| {
  119. TestError::Setup(format!(
  120. "cannot reach shared test Redis at {}: {error}; run `docker compose -f compose.test.yaml up -d`",
  121. redis.url,
  122. ))
  123. })?;
  124. let config = redis::aio::ConnectionManagerConfig::new()
  125. .set_response_timeout(Some(Duration::from_secs(10)))
  126. .set_connection_timeout(Some(Duration::from_secs(5)));
  127. let manager = redis::aio::ConnectionManager::new_with_config(client, config)
  128. .await
  129. .map_err(|error| {
  130. TestError::Setup(format!(
  131. "cannot reach shared test Redis at {}: {error}; run `docker compose -f compose.test.yaml up -d`",
  132. redis.url,
  133. ))
  134. })?;
  135. let (token_prefix, ratelimit_prefix) = unique_prefixes();
  136. let store = Store::from_parts(
  137. token_prefix,
  138. ratelimit_prefix,
  139. d_oauth::config::DurabilityMode::WaitAof,
  140. );
  141. Ok((redis, RedisConn::Single(manager), store))
  142. }
  143. /// The error code of a resolve outcome, or `None` on success.
  144. pub fn code_of<T>(outcome: &Result<T, AppError>) -> Option<OAuthErrorCode> {
  145. match outcome {
  146. Ok(_) => None,
  147. Err(error) => Some(error.code()),
  148. }
  149. }