mod.rs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. //! Postgres repositories over `query!`/`query_as!` macros.
  2. //!
  3. //! Six domain tables: `oauth_clients`, `users`, `scopes`, `user_scopes`,
  4. //! `redirect_uris`, `client_redirect_uris`. No failed-login columns here
  5. //! (those counters are Redis-ephemeral).
  6. pub mod client_repo;
  7. pub mod redirect_repo;
  8. pub mod scope_repo;
  9. pub mod user_repo;
  10. pub mod user_scope_repo;
  11. pub use client_repo::{Client, ClientMetadata, NewClient, TokenEndpointAuthMethod};
  12. pub use redirect_repo::{NewRedirectUri, RedirectKind, RedirectUri, UpsertOutcome};
  13. /// Test-only scaffolding shared by the repository suites.
  14. ///
  15. /// One `postgres:18-alpine` container is started lazily and reused by every test
  16. /// in the lib test binary; each test gets a freshly created database with all
  17. /// embedded migrations applied, so parallel tests stay isolated at one
  18. /// container cost. Integration tests under `tests/` cannot reach this
  19. /// `#[cfg(test)]` module and carry their own container helper instead.
  20. #[cfg(test)]
  21. pub(crate) mod test_support {
  22. use sqlx::{Executor as _, PgPool, postgres::PgPoolOptions};
  23. use testcontainers::{
  24. GenericImage, ImageExt,
  25. core::{IntoContainerPort, WaitFor},
  26. runners::AsyncRunner,
  27. };
  28. use crate::test_guard::{ContainerGuard, Lease, Shared};
  29. /// Boxed test error (no `unwrap`/`expect` under the crate deny lints).
  30. pub(crate) type TestError = Box<dyn std::error::Error + Send + Sync>;
  31. /// Test Postgres superuser password.
  32. const PG_PASSWORD: &str = "db-repo-secret";
  33. /// The shared Postgres server plus the container keeping it alive.
  34. ///
  35. /// The container is a [`ContainerGuard`], so the last test releasing the
  36. /// shared slot removes it synchronously instead of orphaning it.
  37. pub(crate) struct Db {
  38. _container: ContainerGuard,
  39. url: String,
  40. }
  41. static DB: Shared<Db> = Shared::new();
  42. /// Start (once) and return a lease on the shared Postgres server.
  43. ///
  44. /// The lease must stay alive as long as the returned reference is used;
  45. /// dropping the last lease removes the shared container deterministically.
  46. async fn shared_db() -> Result<Lease<Db>, TestError> {
  47. DB.lease::<TestError>(async {
  48. let container = GenericImage::new("postgres", "18-alpine")
  49. .with_exposed_port(5432.tcp())
  50. .with_wait_for(WaitFor::message_on_stderr(
  51. "database system is ready to accept connections",
  52. ))
  53. .with_wait_for(WaitFor::message_on_stdout(
  54. "database system is ready to accept connections",
  55. ))
  56. .with_env_var("POSTGRES_PASSWORD", PG_PASSWORD)
  57. .start()
  58. .await?;
  59. let guarded = ContainerGuard::new(container);
  60. let port = guarded.get_host_port_ipv4(5432).await?;
  61. let url =
  62. format!("postgres://postgres:{PG_PASSWORD}@127.0.0.1:{port}/postgres?sslmode=disable");
  63. Ok(Db {
  64. _container: guarded,
  65. url,
  66. })
  67. })
  68. .await
  69. }
  70. /// A fresh database on the shared server with every embedded migration
  71. /// applied, pooled for the calling test.
  72. ///
  73. /// The returned lease keeps the shared container alive; bind it (usually
  74. /// as `_db`) for the duration of the test.
  75. pub(crate) async fn migrated_pool() -> Result<(Lease<Db>, PgPool), TestError> {
  76. let db = shared_db().await?;
  77. let admin = PgPoolOptions::new()
  78. .max_connections(1)
  79. .connect(&db.url)
  80. .await?;
  81. let name = format!("repo_{}", uuid::Uuid::new_v4().simple());
  82. // `CREATE DATABASE` cannot take a bind parameter and `&Pool`'s `Executor`
  83. // requires a `'static` query; leak this one tiny test-only string.
  84. let ddl: &'static str = Box::leak(format!("CREATE DATABASE \"{name}\"").into_boxed_str());
  85. admin.execute(ddl).await?;
  86. admin.close().await;
  87. let target = db.url.replacen("/postgres?", &format!("/{name}?"), 1);
  88. let pool = PgPoolOptions::new()
  89. .max_connections(4)
  90. .connect(&target)
  91. .await?;
  92. crate::state::MIGRATOR.run(&pool).await?;
  93. Ok((db, pool))
  94. }
  95. }