From 518f4d3d139ac9545c51d378746ee0e85ef500d1 Mon Sep 17 00:00:00 2001 From: Mara Schulke Date: Sat, 4 Nov 2023 18:35:01 +0100 Subject: [PATCH] Setup integration tests for atmosphere --- Cargo.toml | 5 + atmosphere-core/src/schema.rs | 17 +- atmosphere-core/src/testing.rs | 176 +++++++++++++++++- atmosphere-macros/src/schema/table.rs | 5 + examples/forest/main.rs | 35 +--- tests/db/crud.rs | 90 +++++++++ tests/db/migrations/20231101223300_forest.sql | 5 + tests/db/mod.rs | 1 + tests/lib.rs | 1 + 9 files changed, 299 insertions(+), 36 deletions(-) create mode 100644 tests/db/crud.rs create mode 100644 tests/db/migrations/20231101223300_forest.sql create mode 100644 tests/db/mod.rs create mode 100644 tests/lib.rs diff --git a/Cargo.toml b/Cargo.toml index a9769ce..3759460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,11 @@ repository.workspace = true name = "forest" path = "examples/forest/main.rs" +[[test]] +name = "integration" +path = "tests/lib.rs" +test = true + [features] default = ["sqlite", "mysql", "postgres"] any = ["sqlx/any"] diff --git a/atmosphere-core/src/schema.rs b/atmosphere-core/src/schema.rs index 9a358ec..e661a45 100644 --- a/atmosphere-core/src/schema.rs +++ b/atmosphere-core/src/schema.rs @@ -16,6 +16,8 @@ where const PRIMARY_KEY: Column; const FOREIGN_KEYS: &'static [Column]; const DATA: &'static [Column]; + + fn pk(&self) -> &Self::PrimaryKey; } /// A entity is a table that implements [`Create`], [`Read`], [`Update`] & [`Create`] @@ -56,6 +58,7 @@ where self.bind_all(sqlx::query::(&query.into_sql())) .unwrap() + .persistent(false) .execute(executor) .await } @@ -140,7 +143,7 @@ where #[async_trait] pub trait Update: Table + Send + Sync + Unpin + 'static { /// Update the row in the database - async fn update<'e, E>(&mut self, executor: E) -> sqlx::Result + async fn update<'e, E>(&self, executor: E) -> sqlx::Result where Self: Bind + Sync + 'static, E: sqlx::Executor<'e, Database = sqlx::Postgres>, @@ -148,7 +151,7 @@ pub trait Update: Table + Send + Sync + Unpin + 'static { Send + sqlx::IntoArguments<'q, sqlx::Postgres>; /// Save to the database - async fn save<'e, E>(&mut self, executor: E) -> sqlx::Result + async fn save<'e, E>(&self, executor: E) -> sqlx::Result where Self: Bind + Sync + 'static, E: sqlx::Executor<'e, Database = sqlx::Postgres>, @@ -161,7 +164,7 @@ impl Update for T where T: Table + Send + Sync + Unpin + 'static, { - async fn update<'e, E>(&mut self, executor: E) -> sqlx::Result + async fn update<'e, E>(&self, executor: E) -> sqlx::Result where Self: Bind, E: sqlx::Executor<'e, Database = sqlx::Postgres>, @@ -171,10 +174,10 @@ where let query = crate::runtime::sql::SQL::::update().into_sql(); let mut query = sqlx::query::(&query); query = self.bind_all(query).unwrap(); - query.execute(executor).await + query.persistent(false).execute(executor).await } - async fn save<'e, E>(&mut self, executor: E) -> sqlx::Result + async fn save<'e, E>(&self, executor: E) -> sqlx::Result where Self: Bind, E: sqlx::Executor<'e, Database = sqlx::Postgres>, @@ -184,7 +187,7 @@ where let query = crate::runtime::sql::SQL::::upsert().into_sql(); let mut query = sqlx::query::(&query); query = self.bind_all(query).unwrap(); - query.execute(executor).await + query.persistent(false).execute(executor).await } } @@ -232,6 +235,7 @@ where self.bind_all(sqlx::query::(&query.into_sql())) .unwrap() + .persistent(false) .execute(executor) .await } @@ -247,6 +251,7 @@ where sqlx::query::(&query.into_sql()) .bind(pk) + .persistent(false) .execute(executor) .await } diff --git a/atmosphere-core/src/testing.rs b/atmosphere-core/src/testing.rs index 8d50ce9..c7649ff 100644 --- a/atmosphere-core/src/testing.rs +++ b/atmosphere-core/src/testing.rs @@ -1,10 +1,176 @@ -use crate::Entity; +use crate::{Bind, Create, Entity, Read, Update}; +use sqlx::PgPool; +use std::fmt::Debug; -/// Automate integration tests for database entities -pub trait Testable: Entity + Eq + Ord + Clone { - fn mock() -> Self; - fn patch(entity: Self) -> Self; +/// Verify creating of entities +pub async fn create(instance: E, pool: &PgPool) +where + E: Create + Read + Bind + Clone + Debug + Eq + Send, +{ + assert!(E::find(&instance.pk(), pool).await.is_err()); + + instance.create(pool).await.expect("insertion did not work"); + + let retrieved = E::find(&instance.pk(), pool) + .await + .expect("instance not found after insertion"); + + assert_eq!(instance, retrieved); +} + +/// Verify read operations +pub async fn read(instance: E, pool: &PgPool) +where + E: Create + Read + Bind + Clone + Debug + Eq + Send, +{ + assert!(E::find(&instance.pk(), pool).await.is_err()); + + instance.create(pool).await.expect("insertion did not work"); + + let retrieved = E::find(&instance.pk(), pool) + .await + .expect("instance not found after insertion"); + + assert_eq!(instance, retrieved); +} + +/// Verify update operations +pub async fn update(mut instance: E, updates: Vec, pool: &PgPool) +where + E: Read + Update + Bind + Clone + Debug + Eq + Send, +{ + instance.save(pool).await.expect("insertion did not work"); + + for update in updates { + update + .update(pool) + .await + .expect("updating the instance did not work"); + + instance + .reload(pool) + .await + .expect("reloading the instance did not work"); + + assert_eq!(instance, update); + + let retrieved = E::find(&instance.pk(), pool) + .await + .expect("instance not found after update"); + + assert_eq!(instance, retrieved); + } +} + +/// Verify delete operations +pub async fn delete(mut instance: E, pool: &PgPool) +where + E: Entity + Bind + Clone + Debug + Eq + Send, +{ + instance.create(pool).await.expect("insertion did not work"); + + instance.delete(pool).await.expect("deletion did not work"); + + instance + .reload(pool) + .await + .expect_err("instance could be reloaded from db after deletion"); + + println!("until assert"); + + assert!(E::find(&instance.pk(), pool).await.is_err()); + + println!("after assert"); + + instance.create(pool).await.expect("insertion did not work"); + + println!("pre db"); + + E::delete_by(instance.pk(), pool) + .await + .expect("deletion did not work"); + + println!("post db"); + + instance + .reload(pool) + .await + .expect_err("instance could be reloaded from db after deletion"); } // TODO: provide helpers to autogenerate uuids, pks, strings, emails, etc – maybe reexport another // crate? + +// Verify ORM interactions of an [`Entity`] against a live database +//pub async fn verify>(pool: sqlx::PgPool) { +//let a = T::mock(); + +//// verify `create` +//{ +//a.create(&pool).await.unwrap(); +//let db = T::find(a.pk(), &pool).await.unwrap(); +//assert_eq!(a, db); +//} + +//// verify `reload` +//{ +//let mut db = a.clone(); +//db.reload(&pool).await.unwrap(); +//assert_eq!(a, db) +//} + +//// run 256 upserts +//for i in 0..256 { +//let b = T::patch(a.clone(), i); +//assert_ne!(a, b); + +//// verify `save` +//{ +//b.save(&pool).await.unwrap(); +//let mut db = a.clone(); +//db.reload(&pool).await.unwrap(); +//assert_eq!(b, db); +//let db = T::find(a.pk(), &pool).await.unwrap(); +//assert_eq!(a, db); + +//// delete randomly to ensure upsert behavior +//if i % 4 == 0 { +//b.delete(&pool).await.unwrap(); +//} +//} +//} + +//// run 256 updates +//for i in 256..512 { +//let b = T::patch(a.clone(), i); +//assert_ne!(a, b); + +//// verify `update` +//{ +//b.update(&pool).await.unwrap(); +//let mut db = a.clone(); +//db.reload(&pool).await.unwrap(); +//assert_eq!(b, db); +//let db = T::find(a.pk(), &pool).await.unwrap(); +//assert_eq!(a, db); +//} +//} + +//// verify `delete` +//{ +//let c = T::mock(); +//c.create(&pool).await.unwrap(); +//assert_eq!(c, T::find(c.pk(), &pool).await.unwrap()); +//c.delete(&pool).await.unwrap(); +//assert!(T::find(c.pk(), &pool).await.is_err()); +//} + +//// verify `delete_by` +//{ +//let c = T::mock(); +//c.create(&pool).await.unwrap(); +//assert_eq!(c, T::find(c.pk(), &pool).await.unwrap()); +//T::delete_by(c.pk(), &pool).await.unwrap(); +//assert!(T::find(c.pk(), &pool).await.is_err()); +//} +//} diff --git a/atmosphere-macros/src/schema/table.rs b/atmosphere-macros/src/schema/table.rs index e295fe1..bead74c 100644 --- a/atmosphere-macros/src/schema/table.rs +++ b/atmosphere-macros/src/schema/table.rs @@ -74,6 +74,7 @@ impl Table { let schema = schema.to_string(); let pk_ty = &self.primary_key.ty; + let pk_field = &self.primary_key.name; let primary_key = self.primary_key.quote(); let foreign_keys = self.foreign_keys.iter().map(|r| r.column.quote()); let data = self.data.iter().map(|d| d.quote()); @@ -94,6 +95,10 @@ impl Table { const DATA: &'static [::atmosphere::Column<#ident>] = &[ #(#data),* ]; + + fn pk(&self) -> &Self::PrimaryKey { + &self.#pk_field + } } ) } diff --git a/examples/forest/main.rs b/examples/forest/main.rs index 15f8a5f..5778649 100644 --- a/examples/forest/main.rs +++ b/examples/forest/main.rs @@ -1,10 +1,8 @@ -#![allow(unused)] - use atmosphere::prelude::*; use atmosphere_core::Table; -use sqlx::{FromRow, PgPool, Postgres}; +use sqlx::PgPool; -#[derive(Schema, Debug)] +#[derive(Schema, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[table(name = "forest", schema = "public")] struct Forest { #[primary_key] @@ -13,7 +11,7 @@ struct Forest { location: String, } -#[derive(Schema, Debug)] +#[derive(Schema, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[table(name = "tree", schema = "public")] #[relation(grouped_by = Forest)] struct Tree { @@ -27,26 +25,13 @@ struct Tree { async fn main() -> sqlx::Result<()> { let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()).await?; - let mut forest = Forest { - id: 1, - name: "grunewald".to_owned(), - location: "berlin".to_owned(), - }; - - forest.delete(&pool).await?; - forest.create(&pool).await?; - - dbg!(Forest::find(&1i32, &pool).await?); - - forest.name = "test".to_owned(); - forest.update(&pool).await?; - - dbg!(Forest::find(&1i32, &pool).await?); - - forest.name = "test-2".to_owned(); - forest.save(&pool).await?; - - dbg!(Forest::find(&1i32, &pool).await?); + Forest { + id: 0, + name: "test".to_owned(), + location: "germany".to_owned(), + } + .save(&pool) + .await?; Ok(()) } diff --git a/tests/db/crud.rs b/tests/db/crud.rs new file mode 100644 index 0000000..da76884 --- /dev/null +++ b/tests/db/crud.rs @@ -0,0 +1,90 @@ +use atmosphere::prelude::*; +use atmosphere_core::Table; + +#[derive(Schema, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[table(name = "forest", schema = "public")] +struct Forest { + #[primary_key] + id: i32, + name: String, + location: String, +} + +#[derive(Schema, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[table(name = "tree", schema = "public")] +#[relation(grouped_by = Forest)] +struct Tree { + #[primary_key] + id: i32, + #[foreign_key(Forest)] + forest_id: i32, +} + +#[sqlx::test(migrations = "tests/db/migrations")] +async fn create(pool: sqlx::PgPool) { + atmosphere::testing::create( + Forest { + id: 0, + name: "grunewald".to_owned(), + location: "berlin".to_owned(), + }, + &pool, + ) + .await; +} + +#[sqlx::test(migrations = "tests/db/migrations")] +async fn read(pool: sqlx::PgPool) { + atmosphere::testing::read( + Forest { + id: 0, + name: "grunewald".to_owned(), + location: "berlin".to_owned(), + }, + &pool, + ) + .await; +} + +#[sqlx::test(migrations = "tests/db/migrations")] +async fn update(pool: sqlx::PgPool) { + atmosphere::testing::update( + Forest { + id: 0, + name: "grunewald".to_owned(), + location: "berlin".to_owned(), + }, + vec![ + Forest { + id: 0, + name: "gruneeeeeeeewald".to_owned(), + location: "berlin".to_owned(), + }, + Forest { + id: 0, + name: "grunewald".to_owned(), + location: "berlin, germany".to_owned(), + }, + Forest { + id: 0, + name: "englischer garten".to_owned(), + location: "münchen".to_owned(), + }, + ], + &pool, + ) + .await; +} + +#[sqlx::test(migrations = "tests/db/migrations")] +async fn delete(pool: sqlx::PgPool) { + atmosphere::testing::delete( + Forest { + id: 0, + name: "grunewald".to_owned(), + location: "berlin".to_owned(), + }, + &pool, + ) + .await; +} diff --git a/tests/db/migrations/20231101223300_forest.sql b/tests/db/migrations/20231101223300_forest.sql new file mode 100644 index 0000000..f79466f --- /dev/null +++ b/tests/db/migrations/20231101223300_forest.sql @@ -0,0 +1,5 @@ +CREATE TABLE forest ( + id INT PRIMARY KEY, + name TEXT NOT NULL, + location TEXT NOT NULL +); diff --git a/tests/db/mod.rs b/tests/db/mod.rs new file mode 100644 index 0000000..b7c5242 --- /dev/null +++ b/tests/db/mod.rs @@ -0,0 +1 @@ +mod crud; diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..b918018 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1 @@ +mod db;