diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52b42334..6411b4fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,9 +102,14 @@ jobs: test-pgstac: name: Test pgstac runs-on: ubuntu-latest + strategy: + matrix: + pgstac_version: + - v0.8.6 + - v0.9.1 services: pgstac: - image: ghcr.io/stac-utils/pgstac:v0.8.6 + image: ghcr.io/stac-utils/pgstac:${{ matrix.pgstac_version }} env: POSTGRES_USER: username POSTGRES_PASSWORD: password diff --git a/crates/pgstac/Cargo.toml b/crates/pgstac/Cargo.toml index 7ab341a4..a5fab663 100644 --- a/crates/pgstac/Cargo.toml +++ b/crates/pgstac/Cargo.toml @@ -18,6 +18,7 @@ tls = ["dep:rustls", "dep:tokio-postgres-rustls", "dep:webpki-roots"] rustls = { workspace = true, features = ["ring", "std"], optional = true } serde.workspace = true serde_json.workspace = true +stac.workspace = true stac-api.workspace = true thiserror.workspace = true tokio-postgres = { workspace = true, features = ["with-serde_json-1"] } @@ -26,8 +27,7 @@ webpki-roots = { workspace = true, optional = true } [dev-dependencies] geojson.workspace = true -stac.workspace = true -pgstac-test = { path = "pgstac-test" } +rstest.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tokio-test.workspace = true diff --git a/crates/pgstac/pgstac-test/.gitignore b/crates/pgstac/pgstac-test/.gitignore deleted file mode 100644 index ea8c4bf7..00000000 --- a/crates/pgstac/pgstac-test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/crates/pgstac/pgstac-test/Cargo.toml b/crates/pgstac/pgstac-test/Cargo.toml deleted file mode 100644 index a1e8f627..00000000 --- a/crates/pgstac/pgstac-test/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "pgstac-test" -version = "0.0.0" -edition.workspace = true -publish = false - -[lib] -proc-macro = true -test = false -doctest = false - -[dependencies] -quote.workspace = true -syn = { workspace = true, features = ["full", "extra-traits"] } -tokio-postgres.workspace = true diff --git a/crates/pgstac/pgstac-test/src/lib.rs b/crates/pgstac/pgstac-test/src/lib.rs deleted file mode 100644 index 5ae0c547..00000000 --- a/crates/pgstac/pgstac-test/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::ItemFn; - -#[proc_macro_attribute] -pub fn pgstac_test(_args: TokenStream, input: TokenStream) -> TokenStream { - let ast = syn::parse(input).unwrap(); - impl_pgstac_test(ast) -} - -fn impl_pgstac_test(ast: ItemFn) -> TokenStream { - let ident = &ast.sig.ident; - let gen = quote! { - #[tokio::test] - async fn #ident() { - let _mutex = MUTEX.lock().unwrap(); - let config = std::env::var("PGSTAC_RS_TEST_DB") - .unwrap_or("postgresql://username:password@localhost:5432/postgis".to_string()); - let (mut client, connection) = tokio_postgres::connect(&config, tokio_postgres::NoTls).await.unwrap(); - tokio::spawn(async move { - connection.await.unwrap() - }); - let transaction = client.transaction().await.unwrap(); - #ast - #ident(&transaction).await; - transaction.rollback().await.unwrap(); - } - }; - gen.into() -} diff --git a/crates/pgstac/src/lib.rs b/crates/pgstac/src/lib.rs index b6ba2d1f..fd584ab9 100644 --- a/crates/pgstac/src/lib.rs +++ b/crates/pgstac/src/lib.rs @@ -313,43 +313,133 @@ impl Pgstac for T where T: GenericClient {} pub(crate) mod tests { use super::Pgstac; use geojson::{Geometry, Value}; - use pgstac_test::pgstac_test; + use rstest::{fixture, rstest}; use serde_json::{json, Map}; use stac::{Collection, Item}; use stac_api::{Fields, Filter, Search, Sortby}; - use std::sync::Mutex; - use tokio_postgres::Transaction; + use std::{ + ops::Deref, + sync::{atomic::AtomicU16, Mutex}, + }; + use tokio_postgres::{Client, Config, NoTls}; use tokio_test as _; - // This is an absolutely heinous way to ensure that only one test is hitting - // the DB at a time -- the MUTEX is used in the pgstac-test crate as part of - // the code generated by `pgstac_test`. - // - // There's got to be a better way. - pub(crate) static MUTEX: Mutex<()> = Mutex::new(()); + static MUTEX: Mutex<()> = Mutex::new(()); + + struct TestClient { + client: Client, + config: Config, + dbname: String, + } + + impl TestClient { + async fn new(id: u16) -> TestClient { + let dbname = format!("pgstac_test_{}", id); + let config: Config = std::env::var("PGSTAC_RS_TEST_DB") + .unwrap_or("postgresql://username:password@localhost:5432/postgis".to_string()) + .parse() + .unwrap(); + { + let _mutex = MUTEX.lock().unwrap(); + let (client, connection) = config.connect(NoTls).await.unwrap(); + let _handle = tokio::spawn(async move { connection.await.unwrap() }); + let _ = client + .execute( + &format!( + "CREATE DATABASE {} TEMPLATE {}", + dbname, + config.get_dbname().unwrap() + ), + &[], + ) + .await + .unwrap(); + } + let mut test_config = config.clone(); + let (client, connection) = test_config.dbname(&dbname).connect(NoTls).await.unwrap(); + let _handle = tokio::spawn(async move { connection.await.unwrap() }); + TestClient { + client, + config, + dbname, + } + } + + async fn terminate(&mut self) { + let (client, connection) = self.config.connect(NoTls).await.unwrap(); + let _handle = tokio::spawn(async move { connection.await.unwrap() }); + let _ = client + .execute( + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1", + &[&self.dbname], + ) + .await + .unwrap(); + let _ = client + .execute(&format!("DROP DATABASE {}", self.dbname), &[]) + .await + .unwrap(); + } + } + + impl Drop for TestClient { + fn drop(&mut self) { + std::thread::scope(|s| { + let _ = s.spawn(|| { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + runtime.block_on(self.terminate()); + }); + }); + } + } + + impl Deref for TestClient { + type Target = Client; + fn deref(&self) -> &Self::Target { + &self.client + } + } fn longmont() -> Geometry { Geometry::new(Value::Point(vec![-105.1019, 40.1672])) } - #[pgstac_test] - async fn pgstac_version(client: &Transaction<'_>) { + #[fixture] + fn id() -> u16 { + static COUNTER: AtomicU16 = AtomicU16::new(0); + COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } + + #[fixture] + async fn client(id: u16) -> TestClient { + TestClient::new(id).await + } + + #[rstest] + #[tokio::test] + async fn pgstac_version(#[future(awt)] client: TestClient) { let _ = client.pgstac_version().await.unwrap(); } - #[pgstac_test] - async fn context(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn context(#[future(awt)] client: TestClient) { assert!(!client.context().await.unwrap()); } - #[pgstac_test] - async fn set_context(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn set_context(#[future(awt)] client: TestClient) { client.set_context(true).await.unwrap(); assert!(client.context().await.unwrap()); } - #[pgstac_test] - async fn collections(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn collections(#[future(awt)] client: TestClient) { assert!(client.collections().await.unwrap().is_empty()); client .add_collection(Collection::new("an-id", "a description")) @@ -358,16 +448,18 @@ pub(crate) mod tests { assert_eq!(client.collections().await.unwrap().len(), 1); } - #[pgstac_test] - async fn add_collection_duplicate(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn add_collection_duplicate(#[future(awt)] client: TestClient) { assert!(client.collections().await.unwrap().is_empty()); let collection = Collection::new("an-id", "a description"); client.add_collection(collection.clone()).await.unwrap(); assert!(client.add_collection(collection).await.is_err()); } - #[pgstac_test] - async fn upsert_collection(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn upsert_collection(#[future(awt)] client: TestClient) { assert!(client.collections().await.unwrap().is_empty()); let mut collection = Collection::new("an-id", "a description"); client.upsert_collection(collection.clone()).await.unwrap(); @@ -379,8 +471,9 @@ pub(crate) mod tests { ); } - #[pgstac_test] - async fn update_collection(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn update_collection(#[future(awt)] client: TestClient) { let mut collection = Collection::new("an-id", "a description"); client.add_collection(collection.clone()).await.unwrap(); assert!(client @@ -399,19 +492,22 @@ pub(crate) mod tests { ); } - #[pgstac_test] - async fn update_collection_does_not_exit(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn update_collection_does_not_exit(#[future(awt)] client: TestClient) { let collection = Collection::new("an-id", "a description"); assert!(client.update_collection(collection).await.is_err()); } - #[pgstac_test] - async fn collection_not_found(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn collection_not_found(#[future(awt)] client: TestClient) { assert!(client.collection("not-an-id").await.unwrap().is_none()); } - #[pgstac_test] - async fn delete_collection(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn delete_collection(#[future(awt)] client: TestClient) { let collection = Collection::new("an-id", "a description"); client.add_collection(collection.clone()).await.unwrap(); assert!(client.collection("an-id").await.unwrap().is_some()); @@ -419,13 +515,15 @@ pub(crate) mod tests { assert!(client.collection("an-id").await.unwrap().is_none()); } - #[pgstac_test] - async fn delete_collection_does_not_exist(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn delete_collection_does_not_exist(#[future(awt)] client: TestClient) { assert!(client.delete_collection("not-an-id").await.is_err()); } - #[pgstac_test] - async fn item(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn item(#[future(awt)] client: TestClient) { assert!(client .item("an-id", Some("collection-id")) .await @@ -450,14 +548,16 @@ pub(crate) mod tests { ); } - #[pgstac_test] - async fn item_without_collection(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn item_without_collection(#[future(awt)] client: TestClient) { let item = Item::new("an-id"); assert!(client.add_item(item.clone()).await.is_err()); } - #[pgstac_test] - async fn update_item(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn update_item(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -479,8 +579,9 @@ pub(crate) mod tests { ); } - #[pgstac_test] - async fn delete_item(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn delete_item(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -497,8 +598,9 @@ pub(crate) mod tests { ); } - #[pgstac_test] - async fn upsert_item(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn upsert_item(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -508,8 +610,9 @@ pub(crate) mod tests { client.upsert_item(item).await.unwrap(); } - #[pgstac_test] - async fn add_items(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn add_items(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -530,8 +633,9 @@ pub(crate) mod tests { .is_some()); } - #[pgstac_test] - async fn upsert_items(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn upsert_items(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -544,8 +648,9 @@ pub(crate) mod tests { client.upsert_items(&items).await.unwrap(); } - #[pgstac_test] - async fn search_everything(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn search_everything(#[future(awt)] client: TestClient) { assert!(client .search(Search::default()) .await @@ -564,8 +669,9 @@ pub(crate) mod tests { ); } - #[pgstac_test] - async fn search_ids(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn search_ids(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -584,8 +690,9 @@ pub(crate) mod tests { assert!(client.search(search).await.unwrap().features.is_empty()); } - #[pgstac_test] - async fn search_collections(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn search_collections(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -604,8 +711,9 @@ pub(crate) mod tests { assert!(client.search(search).await.unwrap().features.is_empty()); } - #[pgstac_test] - async fn search_limit(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn search_limit(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -618,11 +726,18 @@ pub(crate) mod tests { search.items.limit = Some(1); let page = client.search(search).await.unwrap(); assert_eq!(page.features.len(), 1); - assert_eq!(page.context.limit.unwrap(), 1); + if let Some(context) = page.context { + // v0.8 + assert_eq!(context.limit.unwrap(), 1); + } else { + // v0.9 + assert_eq!(page.number_returned.unwrap(), 1); + } } - #[pgstac_test] - async fn search_bbox(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn search_bbox(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -639,8 +754,9 @@ pub(crate) mod tests { assert!(client.search(search).await.unwrap().features.is_empty()); } - #[pgstac_test] - async fn search_datetime(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn search_datetime(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -658,8 +774,9 @@ pub(crate) mod tests { assert!(client.search(search).await.unwrap().features.is_empty()); } - #[pgstac_test] - async fn search_intersects(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn search_intersects(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -702,8 +819,9 @@ pub(crate) mod tests { assert!(client.search(search).await.unwrap().features.is_empty()); } - #[pgstac_test] - async fn pagination(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn pagination(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -720,18 +838,19 @@ pub(crate) mod tests { assert_eq!(page.features[0]["id"], "an-id"); let _ = search .additional_fields - .insert("token".to_string(), page.next_token().into()); + .insert("token".to_string(), "next:collection-id:an-id".into()); let page = client.search(search.clone()).await.unwrap(); assert_eq!(page.features[0]["id"], "another-id"); let _ = search .additional_fields - .insert("token".to_string(), page.prev_token().into()); + .insert("token".to_string(), "prev:collection-id:another-id".into()); let page = client.search(search).await.unwrap(); assert_eq!(page.features[0]["id"], "an-id"); } - #[pgstac_test] - async fn fields(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn fields(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("an-id"); @@ -757,8 +876,9 @@ pub(crate) mod tests { assert!(item["properties"].as_object().unwrap().get("bar").is_none()); } - #[pgstac_test] - async fn sortby(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn sortby(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("a"); @@ -779,8 +899,9 @@ pub(crate) mod tests { assert_eq!(page.features[1]["id"], "a"); } - #[pgstac_test] - async fn filter(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn filter(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("a"); @@ -806,8 +927,9 @@ pub(crate) mod tests { assert_eq!(page.features.len(), 1); } - #[pgstac_test] - async fn query(client: &Transaction<'_>) { + #[rstest] + #[tokio::test] + async fn query(#[future(awt)] client: TestClient) { let collection = Collection::new("collection-id", "a description"); client.add_collection(collection).await.unwrap(); let mut item = Item::new("a"); diff --git a/crates/pgstac/src/page.rs b/crates/pgstac/src/page.rs index 88f495f4..ca470695 100644 --- a/crates/pgstac/src/page.rs +++ b/crates/pgstac/src/page.rs @@ -1,4 +1,6 @@ use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use stac::Link; use stac_api::{Context, Item}; /// A page of search results. @@ -15,7 +17,26 @@ pub struct Page { pub prev: Option, /// The search context. - pub context: Context, + /// + /// This was removed in pgstac v0.9 + #[serde(default)] + pub context: Option, + + /// The number of values returned. + /// + /// Added in pgstac v0.9 + #[serde(rename = "numberReturned")] + pub number_returned: Option, + + /// Links + /// + /// Added in pgstac v0.9 + #[serde(default)] + pub links: Vec, + + /// Additional fields. + #[serde(flatten)] + pub additional_fields: Map, } impl Page { diff --git a/crates/pgstac/src/tls.rs b/crates/pgstac/src/tls.rs index 99123e48..dbf05814 100644 --- a/crates/pgstac/src/tls.rs +++ b/crates/pgstac/src/tls.rs @@ -96,14 +96,12 @@ impl Default for DummyTlsVerifier { #[cfg(test)] mod tests { - use crate::tests::MUTEX; #[tokio::test] async fn connect() { - let _mutex = MUTEX.lock().unwrap(); - let config = std::env::var("PGSTAC_RS_TEST_DB") - .unwrap_or("postgresql://username:password@localhost:5432/postgis".to_string()); let tls = super::make_unverified_tls(); - let (_, _) = tokio_postgres::connect(&config, tls).await.unwrap(); + let (_, _) = tokio_postgres::connect("host=/var/run/postgresql", tls) + .await + .unwrap(); } } diff --git a/crates/server/src/backend/pgstac.rs b/crates/server/src/backend/pgstac.rs index 9acb78de..f872f6d9 100644 --- a/crates/server/src/backend/pgstac.rs +++ b/crates/server/src/backend/pgstac.rs @@ -158,7 +158,7 @@ where let _ = prev.insert("token".into(), prev_token.into()); item_collection.prev = Some(prev); } - item_collection.context = Some(page.context); + item_collection.context = page.context; Ok(item_collection) } } diff --git a/docker-compose.yml b/docker-compose.yml index 4bf33251..6fdddb39 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: pgstac: container_name: stac-rs - image: ghcr.io/stac-utils/pgstac:v0.8.6 + image: ghcr.io/stac-utils/pgstac:${PGSTAC_VERSION:-v0.9.1} environment: - POSTGRES_USER=username - POSTGRES_PASSWORD=password