tests.rs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. //! `test(grant_refresh)` harness: shared Redis plus builders.
  2. //!
  3. //! Child suites drive the public [`super::super::refresh`] API. Every test
  4. //! uses unique client/grant labels so the shared keyspace never collides.
  5. mod barrier;
  6. mod concurrent;
  7. mod disabled_subject;
  8. mod entitlements;
  9. mod failover;
  10. mod proofs;
  11. mod recovery;
  12. mod reuse;
  13. mod rotation;
  14. mod scope;
  15. use std::{
  16. sync::atomic::{AtomicU64, Ordering},
  17. time::Duration,
  18. };
  19. use secrecy::SecretString;
  20. use testcontainers::{
  21. GenericImage, ImageExt as _,
  22. core::{IntoContainerPort as _, WaitFor},
  23. runners::AsyncRunner as _,
  24. };
  25. use super::RefreshInput;
  26. use crate::{
  27. config::{DurabilityMode, TtlConfig},
  28. error::{AppError, OAuthErrorCode},
  29. keys::{KeyId, KeyRing, KeyRingError},
  30. protocol::{
  31. scope::ScopeSet,
  32. token::{ClientKind, MintRequest, MintedTokens, SenderProof, TokenService, TokenServiceError},
  33. },
  34. state::RedisConn,
  35. store::{Store, StoreError},
  36. test_guard::{ContainerGuard, Lease, Shared},
  37. };
  38. /// Test body result (no `unwrap`/`expect` under the deny lints).
  39. type TestResult = Result<(), TestError>;
  40. /// Errors raised by the refresh-grant test scaffolding itself.
  41. #[derive(Debug, thiserror::Error)]
  42. enum TestError {
  43. /// A testcontainers operation failed.
  44. #[error("container error")]
  45. Containers(#[from] testcontainers::TestcontainersError),
  46. /// A Redis command failed.
  47. #[error("redis error")]
  48. Redis(#[from] redis::RedisError),
  49. /// A store operation failed.
  50. #[error("store error")]
  51. Store(#[from] StoreError),
  52. /// The token service failed.
  53. #[error("service error")]
  54. Service(#[from] TokenServiceError),
  55. /// The key ring was rejected.
  56. #[error("key ring error")]
  57. Keys(#[from] KeyRingError),
  58. /// The grant (or scope parsing) failed.
  59. #[error("app error")]
  60. App(#[from] AppError),
  61. /// A test assertion observed an unexpected value.
  62. #[error("unexpected test value: {0}")]
  63. Unexpected(String),
  64. }
  65. /// One shared Redis 8 container for the whole suite.
  66. static REDIS: Shared<ContainerGuard> = Shared::new();
  67. /// Fresh connection plus store handle against the shared container, with the
  68. /// container URL for opening extra connections (concurrency tests).
  69. async fn test_redis() -> Result<(Lease<ContainerGuard>, RedisConn, Store, String), TestError> {
  70. let container = REDIS
  71. .lease::<TestError>(async {
  72. let started = GenericImage::new("redis", "8-alpine")
  73. .with_exposed_port(6379.tcp())
  74. .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
  75. .with_cmd([
  76. "redis-server",
  77. "--appendonly",
  78. "yes",
  79. "--appendfsync",
  80. "always",
  81. ])
  82. .start()
  83. .await?;
  84. Ok(ContainerGuard::new(started))
  85. })
  86. .await?;
  87. let port = container.get_host_port_ipv4(6379).await?;
  88. let url = format!("redis://127.0.0.1:{port}");
  89. let conn = connect(&url).await?;
  90. let store = Store::from_parts(
  91. "oauth:{state}:".to_owned(),
  92. "oauth:{rl}:".to_owned(),
  93. DurabilityMode::WaitAof,
  94. );
  95. Ok((container, conn, store, url))
  96. }
  97. /// A second connection against the same container URL.
  98. async fn connect(url: &str) -> Result<RedisConn, TestError> {
  99. let client = redis::Client::open(url)?;
  100. let manager_config = redis::aio::ConnectionManagerConfig::new()
  101. .set_response_timeout(Some(Duration::from_secs(10)))
  102. .set_connection_timeout(Some(Duration::from_secs(5)));
  103. let manager = redis::aio::ConnectionManager::new_with_config(client, manager_config).await?;
  104. Ok(RedisConn::Single(manager))
  105. }
  106. static TEST_NONCE: AtomicU64 = AtomicU64::new(50_000);
  107. /// A label no other test uses, so grants/clients never collide.
  108. fn unique(label: &str) -> String {
  109. let n = TEST_NONCE.fetch_add(1, Ordering::Relaxed);
  110. format!("grant-refresh-test-{n}-{label}")
  111. }
  112. /// A key ring with one pepper plus a dummy TOTP key.
  113. fn ring() -> Result<KeyRing, TestError> {
  114. KeyRing::from_parts(
  115. vec![(KeyId::new("v1"), SecretString::from("refresh-test-pepper"))],
  116. vec![(KeyId::new("v1"), SecretString::from("totp-test-secret"))],
  117. vec![(KeyId::new("v1"), SecretString::from("pii-test-secret"))],
  118. )
  119. .map_err(TestError::from)
  120. }
  121. /// A service over the default TTLs (access 300 s, refresh 30 d).
  122. fn service() -> Result<TokenService, TestError> {
  123. TokenService::new(&TtlConfig::default(), &ring()?).map_err(TestError::from)
  124. }
  125. /// Current time as seconds since the Unix epoch.
  126. fn now_unix() -> i64 {
  127. time::OffsetDateTime::now_utc().unix_timestamp()
  128. }
  129. /// A DPoP-only proof for `jkt`.
  130. fn dpop_proof(jkt: &str) -> SenderProof {
  131. SenderProof {
  132. dpop_jkt: Some(jkt.to_owned()),
  133. mtls_x5t_s256: None,
  134. }
  135. }
  136. /// An mTLS-only proof for `x5t`.
  137. fn mtls_proof(x5t: &str) -> SenderProof {
  138. SenderProof {
  139. dpop_jkt: None,
  140. mtls_x5t_s256: Some(x5t.to_owned()),
  141. }
  142. }
  143. /// Mint an initial grant the tests can rotate; returns the minted pair.
  144. async fn mint_grant(
  145. service: &TokenService,
  146. conn: &mut RedisConn,
  147. store: &Store,
  148. client: &str,
  149. kind: ClientKind,
  150. scope: &str,
  151. proof: SenderProof,
  152. ) -> Result<MintedTokens, TestError> {
  153. let minted = service
  154. .mint(
  155. conn,
  156. store,
  157. &MintRequest {
  158. client_id: client.to_owned(),
  159. client_kind: kind,
  160. subject: unique("subject"),
  161. scope: scope.to_owned(),
  162. grant_id: unique("grant"),
  163. proof,
  164. },
  165. now_unix(),
  166. )
  167. .await?;
  168. Ok(minted)
  169. }
  170. /// Baseline refresh inputs; tests override what they probe.
  171. ///
  172. /// Redis-only suites carry no Postgres user, so the client registration and
  173. /// the user entitlement both default to the enabled catalog (fully entitled):
  174. /// scope assertions there probe narrowing/widening only. Suites with a real
  175. /// user build [`RefreshInput`] literally with the current sets instead.
  176. fn input<'a>(
  177. client: &'a str,
  178. kind: ClientKind,
  179. token: &'a str,
  180. scope: Option<&'a str>,
  181. proof: &'a SenderProof,
  182. enabled: &'a ScopeSet,
  183. ) -> RefreshInput<'a> {
  184. RefreshInput {
  185. client_id: client,
  186. client_kind: kind,
  187. refresh_token: token,
  188. requested_scope: scope,
  189. proof,
  190. enabled,
  191. client_registered: enabled,
  192. user_entitled: enabled,
  193. now_unix: now_unix(),
  194. }
  195. }
  196. /// Whether the rejection is `invalid_grant`.
  197. fn is_invalid_grant(error: &AppError) -> bool {
  198. error.code() == OAuthErrorCode::InvalidGrant
  199. }
  200. /// Whether the rejection is `invalid_scope`.
  201. fn is_invalid_scope(error: &AppError) -> bool {
  202. error.code() == OAuthErrorCode::InvalidScope
  203. }