jar_support.rs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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::{collections::HashMap, 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. active: true,
  88. created_at: now,
  89. updated_at: now,
  90. }
  91. }
  92. /// Shared Redis server handle (no per-test container: the URL points at the
  93. /// shared services from `compose.test.yaml`).
  94. ///
  95. /// The lease keeps the shared slot alive; bind it (usually as `_redis`) for
  96. /// the duration of the test.
  97. pub struct JarRedis {
  98. url: String,
  99. }
  100. static JAR_REDIS: Shared<JarRedis> = Shared::new();
  101. /// Resolve the shared Redis URL once; each test connects its own manager.
  102. ///
  103. /// The returned lease keeps the shared slot alive; bind it (usually as
  104. /// `_redis`) for the duration of the test.
  105. async fn shared_redis() -> Result<Lease<JarRedis>, TestError> {
  106. JAR_REDIS
  107. .lease::<TestError>(async { Ok(JarRedis { url: redis_url() }) })
  108. .await
  109. }
  110. /// A connected [`RedisConn`] plus [`Store`] over the shared Redis.
  111. ///
  112. /// The returned lease keeps the shared slot alive; bind it (usually as
  113. /// `_redis`) for the duration of the test. Key prefixes are unique per
  114. /// call, so parallel tests never share state.
  115. pub async fn store_conn() -> Result<(Lease<JarRedis>, RedisConn, Store), TestError> {
  116. let redis = shared_redis().await?;
  117. let client = redis::Client::open(redis.url.as_str()).map_err(|error| {
  118. TestError::Setup(format!(
  119. "cannot reach shared test Redis at {}: {error}; run `docker compose -f compose.test.yaml up -d`",
  120. redis.url,
  121. ))
  122. })?;
  123. let config = redis::aio::ConnectionManagerConfig::new()
  124. .set_response_timeout(Some(Duration::from_secs(10)))
  125. .set_connection_timeout(Some(Duration::from_secs(5)));
  126. let manager = redis::aio::ConnectionManager::new_with_config(client, config)
  127. .await
  128. .map_err(|error| {
  129. TestError::Setup(format!(
  130. "cannot reach shared test Redis at {}: {error}; run `docker compose -f compose.test.yaml up -d`",
  131. redis.url,
  132. ))
  133. })?;
  134. let (token_prefix, ratelimit_prefix) = unique_prefixes();
  135. let store = Store::from_parts(
  136. token_prefix,
  137. ratelimit_prefix,
  138. d_oauth::config::DurabilityMode::WaitAof,
  139. );
  140. Ok((redis, RedisConn::Single(manager), store))
  141. }
  142. /// The error code of a resolve outcome, or `None` on success.
  143. pub fn code_of(outcome: &Result<HashMap<String, String>, AppError>) -> Option<OAuthErrorCode> {
  144. match outcome {
  145. Ok(_) => None,
  146. Err(error) => Some(error.code()),
  147. }
  148. }