test(sandbox): add repository testing
This commit is contained in:
parent
3d93beb5bd
commit
bbec0fc3bf
15 changed files with 545 additions and 124 deletions
|
|
@ -10,5 +10,5 @@ path = "src/main.rs"
|
|||
chrono = "0.4.39"
|
||||
fully_pub = "0.1.4"
|
||||
serde = "1.0.216"
|
||||
sqlx = { version = "0.8.2", features = ["chrono", "uuid", "sqlite"] }
|
||||
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "chrono", "uuid", "migrate"] }
|
||||
sqlxgentools_attrs = { path = "../sqlxgentools_attrs" }
|
||||
|
|
|
|||
26
lib/sandbox/justfile
Normal file
26
lib/sandbox/justfile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Children justfile
|
||||
|
||||
reset-db *args:
|
||||
rm sandbox.db
|
||||
sqlite3 {{args}} sandbox.db < src/migrations/all.sql
|
||||
|
||||
gen-sqlx:
|
||||
../../target/release/sqlx-generator \
|
||||
-m src/models \
|
||||
gen-repositories \
|
||||
-o src/repositories
|
||||
../../target/release/sqlx-generator \
|
||||
-m src/models \
|
||||
gen-migrations \
|
||||
-o src/migrations/all.sql
|
||||
|
||||
gen-sqlx-debug:
|
||||
../../target/debug/sqlx-generator \
|
||||
-m src/models \
|
||||
gen-repositories \
|
||||
-o src/repositories
|
||||
../../target/debug/sqlx-generator \
|
||||
-m src/models \
|
||||
gen-migrations \
|
||||
-o src/migrations/all.sql
|
||||
|
||||
10
lib/sandbox/src/db.rs
Normal file
10
lib/sandbox/src/db.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use fully_pub::fully_pub;
|
||||
use sqlx::{
|
||||
Pool, Sqlite,
|
||||
};
|
||||
|
||||
/// database storage interface
|
||||
#[fully_pub]
|
||||
#[derive(Clone, Debug)]
|
||||
struct Database(Pool<Sqlite>);
|
||||
|
||||
3
lib/sandbox/src/lib.rs
Normal file
3
lib/sandbox/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod repositories;
|
||||
pub mod db;
|
||||
pub mod models;
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
pub mod models;
|
||||
pub mod repositories;
|
||||
pub mod db;
|
||||
|
||||
fn main() {
|
||||
println!("Sandbox")
|
||||
|
|
|
|||
18
lib/sandbox/src/migrations/all.sql
Normal file
18
lib/sandbox/src/migrations/all.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
CREATE TABLE usersss (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
handle TEXT NOT NULL UNIQUE,
|
||||
full_name TEXT,
|
||||
prefered_color INTEGER,
|
||||
last_login_at DATETIME,
|
||||
status TEXT NOT NULL,
|
||||
groups TEXT NOT NULL,
|
||||
avatar_bytes BLOB NOT NULL
|
||||
);
|
||||
CREATE TABLE user_tokens (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
last_use_time DATETIME,
|
||||
creation_time DATETIME NOT NULL,
|
||||
expiration_time DATETIME NOT NULL
|
||||
);
|
||||
1
lib/sandbox/src/models/mod.rs
Normal file
1
lib/sandbox/src/models/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod user;
|
||||
|
|
@ -5,6 +5,7 @@ use fully_pub::fully_pub;
|
|||
use sqlxgentools_attrs::{sql_generator_model, SqlGeneratorDerive};
|
||||
|
||||
#[derive(sqlx::Type, Clone, Debug, PartialEq)]
|
||||
#[fully_pub]
|
||||
enum UserStatus {
|
||||
Disabled,
|
||||
Invited,
|
||||
|
|
@ -34,7 +35,7 @@ struct User {
|
|||
struct UserToken {
|
||||
#[sql_generator_field(is_primary=true)]
|
||||
id: String,
|
||||
#[sql_generator_field(foreign_key=Relation::BelongsTo(User))]
|
||||
// #[sql_generator_field(foreign_key=Relation::BelongsTo(User))]
|
||||
user_id: String,
|
||||
secret: String,
|
||||
last_use_time: Option<DateTime<Utc>>,
|
||||
|
|
@ -42,4 +43,3 @@ struct UserToken {
|
|||
expiration_time: DateTime<Utc>
|
||||
}
|
||||
|
||||
|
||||
2
lib/sandbox/src/repositories/mod.rs
Normal file
2
lib/sandbox/src/repositories/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod user_repository;
|
||||
pub mod user_token_repository;
|
||||
115
lib/sandbox/src/repositories/user_repository.rs
Normal file
115
lib/sandbox/src/repositories/user_repository.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use crate::models::user::User;
|
||||
use crate::db::Database;
|
||||
pub struct UserRepository {
|
||||
db: Database,
|
||||
}
|
||||
impl UserRepository {
|
||||
pub fn new(db: Database) -> Self {
|
||||
UserRepository { db }
|
||||
}
|
||||
pub async fn get_all(&self) -> Result<Vec<User>, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>("SELECT * FROM usersss").fetch_all(&self.db.0).await
|
||||
}
|
||||
pub async fn get_by_id(&self, item_id: &str) -> Result<User, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>("SELECT * FROM usersss WHERE id = $1")
|
||||
.bind(item_id)
|
||||
.fetch_one(&self.db.0)
|
||||
.await
|
||||
}
|
||||
pub async fn get_many_by_id(
|
||||
&self,
|
||||
items_ids: &[&str],
|
||||
) -> Result<Vec<User>, sqlx::Error> {
|
||||
if items_ids.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let placeholder_params: String = (1..=(items_ids.len()))
|
||||
.map(|i| format!("${}", i))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
let query_sql = format!(
|
||||
"SELECT * FROM usersss WHERE id IN ({})", placeholder_params
|
||||
);
|
||||
let mut query = sqlx::query_as::<_, User>(&query_sql);
|
||||
for id in items_ids {
|
||||
query = query.bind(id);
|
||||
}
|
||||
query.fetch_all(&self.db.0).await
|
||||
}
|
||||
pub async fn insert(&self, entity: &User) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"INSERT INTO usersss (id, handle, full_name, prefered_color, last_login_at, status, groups, avatar_bytes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
)
|
||||
.bind(&entity.id)
|
||||
.bind(&entity.handle)
|
||||
.bind(&entity.full_name)
|
||||
.bind(&entity.prefered_color)
|
||||
.bind(&entity.last_login_at)
|
||||
.bind(&entity.status)
|
||||
.bind(&entity.groups)
|
||||
.bind(&entity.avatar_bytes)
|
||||
.execute(&self.db.0)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn insert_many(&self, entities: &Vec<User>) -> Result<(), sqlx::Error> {
|
||||
let values_templates: String = (1..(8usize * entities.len() + 1))
|
||||
.collect::<Vec<usize>>()
|
||||
.chunks(8usize)
|
||||
.map(|c| c.to_vec())
|
||||
.map(|x| {
|
||||
format!(
|
||||
"({})", x.iter().map(| i | format!("${}", i)).collect:: < Vec <
|
||||
String >> ().join(", ")
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
let query_sql = format!(
|
||||
"INSERT INTO usersss (id, handle, full_name, prefered_color, last_login_at, status, groups, avatar_bytes) VALUES {} ON CONFLICT DO NOTHING",
|
||||
values_templates
|
||||
);
|
||||
let mut query = sqlx::query(&query_sql);
|
||||
for entity in entities {
|
||||
query = query
|
||||
.bind(&entity.id)
|
||||
.bind(&entity.handle)
|
||||
.bind(&entity.full_name)
|
||||
.bind(&entity.prefered_color)
|
||||
.bind(&entity.last_login_at)
|
||||
.bind(&entity.status)
|
||||
.bind(&entity.groups)
|
||||
.bind(&entity.avatar_bytes);
|
||||
}
|
||||
query.execute(&self.db.0).await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn update_by_id(
|
||||
&self,
|
||||
item_id: &str,
|
||||
entity: &User,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE usersss SET id = $2, handle = $3, full_name = $4, prefered_color = $5, last_login_at = $6, status = $7, groups = $8, avatar_bytes = $9 WHERE id = $1",
|
||||
)
|
||||
.bind(item_id)
|
||||
.bind(&entity.id)
|
||||
.bind(&entity.handle)
|
||||
.bind(&entity.full_name)
|
||||
.bind(&entity.prefered_color)
|
||||
.bind(&entity.last_login_at)
|
||||
.bind(&entity.status)
|
||||
.bind(&entity.groups)
|
||||
.bind(&entity.avatar_bytes)
|
||||
.execute(&self.db.0)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn delete_by_id(&self, item_id: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("DELETE FROM usersss WHERE id = $1")
|
||||
.bind(item_id)
|
||||
.execute(&self.db.0)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
114
lib/sandbox/src/repositories/user_token_repository.rs
Normal file
114
lib/sandbox/src/repositories/user_token_repository.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use crate::models::user::UserToken;
|
||||
use crate::db::Database;
|
||||
pub struct UserTokenRepository {
|
||||
db: Database,
|
||||
}
|
||||
impl UserTokenRepository {
|
||||
pub fn new(db: Database) -> Self {
|
||||
UserTokenRepository { db }
|
||||
}
|
||||
pub async fn get_all(&self) -> Result<Vec<UserToken>, sqlx::Error> {
|
||||
sqlx::query_as::<_, UserToken>("SELECT * FROM user_tokens")
|
||||
.fetch_all(&self.db.0)
|
||||
.await
|
||||
}
|
||||
pub async fn get_by_id(&self, item_id: &str) -> Result<UserToken, sqlx::Error> {
|
||||
sqlx::query_as::<_, UserToken>("SELECT * FROM user_tokens WHERE id = $1")
|
||||
.bind(item_id)
|
||||
.fetch_one(&self.db.0)
|
||||
.await
|
||||
}
|
||||
pub async fn get_many_by_id(
|
||||
&self,
|
||||
items_ids: &[&str],
|
||||
) -> Result<Vec<UserToken>, sqlx::Error> {
|
||||
if items_ids.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let placeholder_params: String = (1..=(items_ids.len()))
|
||||
.map(|i| format!("${}", i))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
let query_sql = format!(
|
||||
"SELECT * FROM user_tokens WHERE id IN ({})", placeholder_params
|
||||
);
|
||||
let mut query = sqlx::query_as::<_, UserToken>(&query_sql);
|
||||
for id in items_ids {
|
||||
query = query.bind(id);
|
||||
}
|
||||
query.fetch_all(&self.db.0).await
|
||||
}
|
||||
pub async fn insert(&self, entity: &UserToken) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"INSERT INTO user_tokens (id, user_id, secret, last_use_time, creation_time, expiration_time) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
)
|
||||
.bind(&entity.id)
|
||||
.bind(&entity.user_id)
|
||||
.bind(&entity.secret)
|
||||
.bind(&entity.last_use_time)
|
||||
.bind(&entity.creation_time)
|
||||
.bind(&entity.expiration_time)
|
||||
.execute(&self.db.0)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn insert_many(
|
||||
&self,
|
||||
entities: &Vec<UserToken>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
let values_templates: String = (1..(6usize * entities.len() + 1))
|
||||
.collect::<Vec<usize>>()
|
||||
.chunks(6usize)
|
||||
.map(|c| c.to_vec())
|
||||
.map(|x| {
|
||||
format!(
|
||||
"({})", x.iter().map(| i | format!("${}", i)).collect:: < Vec <
|
||||
String >> ().join(", ")
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
let query_sql = format!(
|
||||
"INSERT INTO user_tokens (id, user_id, secret, last_use_time, creation_time, expiration_time) VALUES {} ON CONFLICT DO NOTHING",
|
||||
values_templates
|
||||
);
|
||||
let mut query = sqlx::query(&query_sql);
|
||||
for entity in entities {
|
||||
query = query
|
||||
.bind(&entity.id)
|
||||
.bind(&entity.user_id)
|
||||
.bind(&entity.secret)
|
||||
.bind(&entity.last_use_time)
|
||||
.bind(&entity.creation_time)
|
||||
.bind(&entity.expiration_time);
|
||||
}
|
||||
query.execute(&self.db.0).await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn update_by_id(
|
||||
&self,
|
||||
item_id: &str,
|
||||
entity: &UserToken,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE user_tokens SET id = $2, user_id = $3, secret = $4, last_use_time = $5, creation_time = $6, expiration_time = $7 WHERE id = $1",
|
||||
)
|
||||
.bind(item_id)
|
||||
.bind(&entity.id)
|
||||
.bind(&entity.user_id)
|
||||
.bind(&entity.secret)
|
||||
.bind(&entity.last_use_time)
|
||||
.bind(&entity.creation_time)
|
||||
.bind(&entity.expiration_time)
|
||||
.execute(&self.db.0)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn delete_by_id(&self, item_id: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("DELETE FROM user_tokens WHERE id = $1")
|
||||
.bind(item_id)
|
||||
.execute(&self.db.0)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
86
lib/sandbox/tests/test_user_repository.rs
Normal file
86
lib/sandbox/tests/test_user_repository.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#![feature(assert_matches)]
|
||||
|
||||
// integration tests
|
||||
|
||||
use std::assert_matches::assert_matches;
|
||||
|
||||
use chrono::Utc;
|
||||
use sandbox::{models::user::{User, UserStatus}, repositories::user_repository::UserRepository};
|
||||
use sqlx::{types::Json, Pool, Sqlite};
|
||||
|
||||
#[sqlx::test(fixtures("../src/migrations/all.sql"))]
|
||||
async fn test_user_repository_create_read_update_delete(pool: Pool<Sqlite>) -> sqlx::Result<()> {
|
||||
pool.acquire().await?;
|
||||
|
||||
// Create
|
||||
let user_repo = UserRepository::new(sandbox::db::Database(pool));
|
||||
let new_user = User {
|
||||
id: "ffffffff-0000-4000-0000-0000000000c9".into(),
|
||||
handle: "caravage".into(),
|
||||
full_name: Some("Michelangelo Merisi da Caravaggio".into()),
|
||||
prefered_color: Some(0xff00ff.into()),
|
||||
last_login_at: Some(Utc::now()),
|
||||
status: UserStatus::Invited,
|
||||
groups: Json(vec!["artists".into()]),
|
||||
avatar_bytes: vec![0x00]
|
||||
};
|
||||
assert_matches!(
|
||||
user_repo.insert(&new_user).await,
|
||||
Ok(())
|
||||
);
|
||||
assert_matches!(
|
||||
user_repo.get_by_id("ffffffff-0000-4000-0000-0000000000c9".into()).await,
|
||||
Ok(User { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
user_repo.get_by_id("ffffffff-0000-4040-0000-000000000000".into()).await,
|
||||
Err(sqlx::Error::RowNotFound)
|
||||
);
|
||||
|
||||
// Insert Many
|
||||
let bunch_of_users: Vec<User> = (0..10).map(|pid| User {
|
||||
id: format!("ffffffff-0000-4000-0010-{:0>8}", pid),
|
||||
handle: format!("user num {}", pid),
|
||||
full_name: None,
|
||||
prefered_color: None,
|
||||
last_login_at: None,
|
||||
status: UserStatus::Invited,
|
||||
groups: Json(vec![]),
|
||||
avatar_bytes: vec![]
|
||||
}).collect();
|
||||
assert_matches!(
|
||||
user_repo.insert_many(&bunch_of_users).await,
|
||||
Ok(())
|
||||
);
|
||||
|
||||
// Read many all
|
||||
let read_all_res = user_repo.get_all().await;
|
||||
assert_matches!(
|
||||
read_all_res,
|
||||
Ok(..)
|
||||
);
|
||||
let all_users = read_all_res.unwrap();
|
||||
assert_eq!(all_users.len(), 11);
|
||||
|
||||
// Update
|
||||
let mut updated_user = new_user.clone();
|
||||
updated_user.status = UserStatus::Disabled;
|
||||
assert_matches!(
|
||||
user_repo.update_by_id(&new_user.id, &updated_user).await,
|
||||
Ok(())
|
||||
);
|
||||
let user_from_db = user_repo.get_by_id("ffffffff-0000-4000-0000-0000000000c9".into()).await.unwrap();
|
||||
assert_eq!(user_from_db.status, UserStatus::Disabled);
|
||||
|
||||
// Delete
|
||||
assert_matches!(
|
||||
user_repo.delete_by_id(&new_user.id).await,
|
||||
Ok(())
|
||||
);
|
||||
assert_matches!(
|
||||
user_repo.get_by_id("ffffffff-0000-4000-0000-0000000000c9".into()).await,
|
||||
Err(sqlx::Error::RowNotFound)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -235,7 +235,6 @@ fn generate_repository_file(model: &Model) -> Result<SourceNodeContainer> {
|
|||
let base_repository_code: TokenStream = quote! {
|
||||
use crate::models::#resource_module_ident::#resource_ident;
|
||||
use crate::db::Database;
|
||||
use anyhow::{Result, Context};
|
||||
|
||||
pub struct #repository_ident {
|
||||
db: Database
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue