migration_resource_servers.rs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. //! Schema contract for the `resource_servers` catalog and the per-client
  2. //! `oauth_clients.access_token_format` column (plan todo 2).
  3. //!
  4. //! Compose-first: the test connects to the shared external Postgres started by
  5. //! `docker compose -f compose.test.yaml up -d` and applies every embedded
  6. //! migration to a freshly created database, matching the rest of the migration
  7. //! suites.
  8. //!
  9. //! The single test name contains `resource_catalog_crud` so
  10. //! `cargo nextest run -E 'test(resource_catalog_crud)'` selects exactly it.
  11. mod common;
  12. use common::{TestError, insert_client, is_unique_violation};
  13. /// The catalog round-trips insert / list / disable, rejects a duplicate
  14. /// identifier, and `oauth_clients.access_token_format` accepts only the
  15. /// `opaque`/`jwt` pair while every existing row reads back `opaque`.
  16. #[tokio::test]
  17. async fn resource_catalog_crud() -> Result<(), TestError> {
  18. let pool = common::fresh_migrated_db().await?;
  19. // Insert -> read back -> list.
  20. let id = uuid::Uuid::new_v4();
  21. sqlx::query(
  22. "INSERT INTO resource_servers (id, identifier, description, created_at) \
  23. VALUES ($1, $2, $3, now())",
  24. )
  25. .bind(id)
  26. .bind("https://api.example.test")
  27. .bind("Example API")
  28. .execute(&pool)
  29. .await?;
  30. let (identifier, description, enabled): (String, String, bool) =
  31. sqlx::query_as("SELECT identifier, description, enabled FROM resource_servers WHERE id = $1")
  32. .bind(id)
  33. .fetch_one(&pool)
  34. .await?;
  35. assert_eq!(
  36. identifier, "https://api.example.test",
  37. "identifier round-trips"
  38. );
  39. assert_eq!(description, "Example API", "description round-trips");
  40. assert!(enabled, "a new resource server is enabled by default");
  41. let listed: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM resource_servers")
  42. .fetch_one(&pool)
  43. .await?;
  44. assert_eq!(listed, 1, "the inserted resource server is listed");
  45. // Disable round-trip.
  46. let updated = sqlx::query("UPDATE resource_servers SET enabled = false WHERE id = $1")
  47. .bind(id)
  48. .execute(&pool)
  49. .await?;
  50. assert_eq!(
  51. updated.rows_affected(),
  52. 1,
  53. "disable updates exactly one row"
  54. );
  55. let enabled_after: bool =
  56. sqlx::query_scalar("SELECT enabled FROM resource_servers WHERE id = $1")
  57. .bind(id)
  58. .fetch_one(&pool)
  59. .await?;
  60. assert!(!enabled_after, "the resource server is disabled");
  61. // A duplicate identifier is rejected by the unique constraint.
  62. let duplicate =
  63. sqlx::query("INSERT INTO resource_servers (id, identifier, created_at) VALUES ($1, $2, now())")
  64. .bind(uuid::Uuid::new_v4())
  65. .bind("https://api.example.test")
  66. .execute(&pool)
  67. .await
  68. .err();
  69. assert!(
  70. matches!(duplicate.as_ref(), Some(err) if is_unique_violation(err)),
  71. "duplicate identifier was accepted: {duplicate:?}"
  72. );
  73. // Omitted columns take their defaults (empty description, enabled true).
  74. let default_id = uuid::Uuid::new_v4();
  75. sqlx::query("INSERT INTO resource_servers (id, identifier, created_at) VALUES ($1, $2, now())")
  76. .bind(default_id)
  77. .bind("https://other.example.test")
  78. .execute(&pool)
  79. .await?;
  80. let (description, enabled): (String, bool) =
  81. sqlx::query_as("SELECT description, enabled FROM resource_servers WHERE id = $1")
  82. .bind(default_id)
  83. .fetch_one(&pool)
  84. .await?;
  85. assert_eq!(description, "", "description defaults to empty");
  86. assert!(enabled, "enabled defaults to true");
  87. // An existing client reads back `opaque` without setting the column.
  88. insert_client(
  89. &pool,
  90. uuid::Uuid::new_v4(),
  91. "resource-catalog-client",
  92. None,
  93. None,
  94. )
  95. .await?;
  96. let format: String =
  97. sqlx::query_scalar("SELECT access_token_format FROM oauth_clients WHERE client_id = $1")
  98. .bind("resource-catalog-client")
  99. .fetch_one(&pool)
  100. .await?;
  101. assert_eq!(
  102. format, "opaque",
  103. "an omitted access_token_format defaults to opaque"
  104. );
  105. // The `jwt` value is accepted.
  106. sqlx::query("UPDATE oauth_clients SET access_token_format = 'jwt' WHERE client_id = $1")
  107. .bind("resource-catalog-client")
  108. .execute(&pool)
  109. .await?;
  110. // An unknown format is rejected by the CHECK constraint.
  111. let bad =
  112. sqlx::query("UPDATE oauth_clients SET access_token_format = 'bogus' WHERE client_id = $1")
  113. .bind("resource-catalog-client")
  114. .execute(&pool)
  115. .await
  116. .err();
  117. assert!(
  118. matches!(bad.as_ref(), Some(sqlx::Error::Database(db)) if db.code().as_deref() == Some("23514")),
  119. "the CHECK must reject an unknown access_token_format: {bad:?}"
  120. );
  121. Ok(())
  122. }