jar_support.rs 5.5 KB

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