mod.rs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. //! Shared scaffolding for Postgres-backed migration integration tests.
  2. //!
  3. //! The migration SQL is embedded byte-for-byte from the repo so assertions
  4. //! run against the exact shipped file. Test bodies use `Result` and `?`
  5. //! (`unwrap`/`expect`/`panic` are denied workspace-wide).
  6. use sqlx::{PgPool, Row as _};
  7. use testcontainers::{
  8. GenericImage, ImageExt,
  9. core::{IntoContainerPort, WaitFor},
  10. runners::AsyncRunner,
  11. };
  12. #[path = "../../src/test_guard.rs"]
  13. #[allow(dead_code)]
  14. mod test_guard;
  15. use test_guard::ContainerGuard;
  16. /// The `oauth_clients` migration under test (todo 8).
  17. pub const OAUTH_CLIENTS_SQL: &str = include_str!("../../migrations/1791072000_oauth_clients.sql");
  18. /// Version encoded in the `oauth_clients` migration filename.
  19. pub const OAUTH_CLIENTS_VERSION: i64 = 1_791_072_000;
  20. /// Throwaway Postgres superuser password.
  21. const PG_PASSWORD: &str = "migration-clients-secret";
  22. /// Aggregate error so test bodies use `?` instead of panicking.
  23. #[derive(Debug, thiserror::Error)]
  24. pub enum TestError {
  25. /// A testcontainers operation failed.
  26. #[error("container error: {0}")]
  27. Containers(#[from] testcontainers::TestcontainersError),
  28. /// A sqlx operation failed.
  29. #[error("database error: {0}")]
  30. Database(#[from] sqlx::Error),
  31. }
  32. /// Start a fresh Postgres and connect a pool to it.
  33. ///
  34. /// The returned guard must stay alive for the duration of the test; dropping
  35. /// it removes the container synchronously.
  36. pub async fn postgres() -> Result<(ContainerGuard, PgPool), TestError> {
  37. let container = GenericImage::new("postgres", "latest")
  38. .with_exposed_port(5432.tcp())
  39. .with_wait_for(WaitFor::message_on_stderr(
  40. "database system is ready to accept connections",
  41. ))
  42. .with_wait_for(WaitFor::message_on_stdout(
  43. "database system is ready to accept connections",
  44. ))
  45. .with_env_var("POSTGRES_PASSWORD", PG_PASSWORD)
  46. .start()
  47. .await?;
  48. let guarded = ContainerGuard::new(container);
  49. let port = guarded.get_host_port_ipv4(5432).await?;
  50. let url = format!("postgres://postgres:{PG_PASSWORD}@127.0.0.1:{port}/postgres?sslmode=disable");
  51. let pool = PgPool::connect(&url).await?;
  52. Ok((guarded, pool))
  53. }
  54. /// Apply a migration script exactly as the migrator would (multi-statement).
  55. ///
  56. /// `sqlx::raw_sql` requires a `'static` string, so callers pass an embedded
  57. /// `include_str!` constant.
  58. pub async fn migrate(pool: &PgPool, sql: &'static str) -> Result<(), TestError> {
  59. sqlx::raw_sql(sql).execute(pool).await?;
  60. Ok(())
  61. }
  62. /// Insert one `oauth_clients` row, returning the raw driver error (if any).
  63. pub async fn insert_client(
  64. pool: &PgPool,
  65. id: uuid::Uuid,
  66. client_id: &str,
  67. subject_dn: Option<&str>,
  68. thumbprint: Option<&str>,
  69. ) -> Result<(), sqlx::Error> {
  70. sqlx::query(
  71. "INSERT INTO oauth_clients (
  72. id, client_id, client_id_issued_at, client_name,
  73. grant_types, response_types, scope, token_endpoint_auth_method,
  74. tls_client_auth_subject_dn, tls_client_auth_cert_thumbprint,
  75. created_at, updated_at
  76. ) VALUES ($1, $2, now(), $3,
  77. ARRAY['client_credentials'], ARRAY['token'], 'api:read',
  78. 'client_secret_basic', $4, $5, now(), now())",
  79. )
  80. .bind(id)
  81. .bind(client_id)
  82. .bind("Migration Test Client")
  83. .bind(subject_dn)
  84. .bind(thumbprint)
  85. .execute(pool)
  86. .await
  87. .map(|_| ())
  88. }
  89. /// Whether `err` is Postgres `unique_violation` (SQLSTATE 23505).
  90. pub fn is_unique_violation(err: &sqlx::Error) -> bool {
  91. matches!(err, sqlx::Error::Database(db) if db.code().as_deref() == Some("23505"))
  92. }
  93. /// Collect `oauth_clients` columns as `(name, udt_name, is_nullable)`.
  94. pub async fn client_columns(
  95. pool: &PgPool,
  96. ) -> Result<std::collections::BTreeSet<(String, String, String)>, TestError> {
  97. let rows = sqlx::query(
  98. "SELECT column_name, udt_name, is_nullable
  99. FROM information_schema.columns
  100. WHERE table_schema = 'public' AND table_name = 'oauth_clients'",
  101. )
  102. .fetch_all(pool)
  103. .await?;
  104. let mut columns = std::collections::BTreeSet::new();
  105. for row in &rows {
  106. columns.insert((
  107. row.try_get::<String, _>("column_name")?,
  108. row.try_get::<String, _>("udt_name")?,
  109. row.try_get::<String, _>("is_nullable")?,
  110. ));
  111. }
  112. Ok(columns)
  113. }
  114. /// Collect `oauth_clients` indexes as `indexname -> indexdef`.
  115. pub async fn client_indexes(
  116. pool: &PgPool,
  117. ) -> Result<std::collections::BTreeMap<String, String>, TestError> {
  118. let rows = sqlx::query(
  119. "SELECT indexname, indexdef
  120. FROM pg_indexes
  121. WHERE schemaname = 'public' AND tablename = 'oauth_clients'",
  122. )
  123. .fetch_all(pool)
  124. .await?;
  125. let mut defs = std::collections::BTreeMap::new();
  126. for row in &rows {
  127. defs.insert(
  128. row.try_get::<String, _>("indexname")?,
  129. row.try_get::<String, _>("indexdef")?,
  130. );
  131. }
  132. Ok(defs)
  133. }