Add ability to list groups and their users

This commit is contained in:
Valentin Tolmer 2021-04-14 20:52:38 +02:00
parent f198638f99
commit c48da8b758
3 changed files with 105 additions and 19 deletions

View File

@ -28,7 +28,7 @@ tracing-actix-web = "0.3.0-beta.2"
tracing-log = "*" tracing-log = "*"
tracing-subscriber = "*" tracing-subscriber = "*"
async-trait = "0.1.48" async-trait = "0.1.48"
sea-query = { version = "0.9.2", features = [ "with-chrono" ] } sea-query = { version = "0.9.4", features = [ "with-chrono" ] }
[dependencies.figment] [dependencies.figment]
features = ["toml", "env"] features = ["toml", "env"]

View File

@ -4,6 +4,7 @@ use crate::infra::configuration::Configuration;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use async_trait::async_trait; use async_trait::async_trait;
use futures_util::StreamExt; use futures_util::StreamExt;
use futures_util::TryStreamExt;
use log::*; use log::*;
use sea_query::{Expr, Order, Query, SimpleExpr, SqliteQueryBuilder}; use sea_query::{Expr, Order, Query, SimpleExpr, SqliteQueryBuilder};
use sqlx::Row; use sqlx::Row;
@ -40,10 +41,17 @@ pub struct User {
pub creation_date: chrono::NaiveDateTime, pub creation_date: chrono::NaiveDateTime,
} }
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct Group {
pub display_name: String,
pub users: Vec<String>,
}
#[async_trait] #[async_trait]
pub trait BackendHandler: Clone + Send { pub trait BackendHandler: Clone + Send {
async fn bind(&self, request: BindRequest) -> Result<()>; async fn bind(&self, request: BindRequest) -> Result<()>;
async fn list_users(&self, request: ListUsersRequest) -> Result<Vec<User>>; async fn list_users(&self, request: ListUsersRequest) -> Result<Vec<User>>;
async fn list_groups(&self) -> Result<Vec<Group>>;
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -141,6 +149,49 @@ impl BackendHandler for SqlBackendHandler {
Ok(results.into_iter().collect::<sqlx::Result<Vec<User>>>()?) Ok(results.into_iter().collect::<sqlx::Result<Vec<User>>>()?)
} }
async fn list_groups(&self) -> Result<Vec<Group>> {
let query: String = Query::select()
.column(Groups::DisplayName)
.column(Memberships::UserId)
.from(Groups::Table)
.left_join(
Memberships::Table,
Expr::tbl(Groups::Table, Groups::GroupId)
.equals(Memberships::Table, Memberships::GroupId),
)
.order_by(Groups::DisplayName, Order::Asc)
.order_by(Memberships::UserId, Order::Asc)
.to_string(SqliteQueryBuilder);
let mut results = sqlx::query(&query).fetch(&self.sql_pool);
let mut groups = Vec::new();
// The rows are ordered by group, user, so we need to group them into vectors.
{
let mut current_group = String::new();
let mut current_users = Vec::new();
while let Some(row) = results.try_next().await? {
let display_name = row.get::<String, _>("display_name");
if display_name != current_group {
if current_group != "" {
groups.push(Group {
display_name: current_group,
users: current_users,
});
current_users = Vec::new();
}
current_group = display_name.clone();
}
current_users.push(row.get::<String, _>("user_id"));
}
groups.push(Group {
display_name: current_group,
users: current_users,
});
}
Ok(groups)
}
} }
#[cfg(test)] #[cfg(test)]
@ -153,6 +204,7 @@ mockall::mock! {
impl BackendHandler for TestBackendHandler { impl BackendHandler for TestBackendHandler {
async fn bind(&self, request: BindRequest) -> Result<()>; async fn bind(&self, request: BindRequest) -> Result<()>;
async fn list_users(&self, request: ListUsersRequest) -> Result<Vec<User>>; async fn list_users(&self, request: ListUsersRequest) -> Result<Vec<User>>;
async fn list_groups(&self) -> Result<Vec<Group>>;
} }
} }
@ -172,7 +224,6 @@ mod tests {
} }
async fn insert_user(sql_pool: &Pool, name: &str, pass: &str) { async fn insert_user(sql_pool: &Pool, name: &str, pass: &str) {
/*
let query = Query::insert() let query = Query::insert()
.into_table(Users::Table) .into_table(Users::Table)
.columns(vec![ .columns(vec![
@ -185,27 +236,34 @@ mod tests {
Users::Password, Users::Password,
]) ])
.values_panic(vec![ .values_panic(vec![
"bob".into(), name.into(),
"bob@bob".into(), "bob@bob".into(),
"Bob Böbberson".into(), "Bob Böbberson".into(),
"Bob".into(), "Bob".into(),
"Böbberson".into(), "Böbberson".into(),
chrono::NaiveDateTime::from_timestamp(0, 0).into(), chrono::NaiveDateTime::from_timestamp(0, 0).into(),
"bob00".into(), pass.into(),
]) ])
.to_string(SqliteQueryBuilder); .to_string(SqliteQueryBuilder);
sqlx::query(&query).execute(&sql_pool).await.unwrap(); sqlx::query(&query).execute(sql_pool).await.unwrap();
*/ }
sqlx::query(
r#"INSERT INTO users async fn insert_group(sql_pool: &Pool, id: u32, name: &str) {
(user_id, email, display_name, first_name, last_name, creation_date, password) let query = Query::insert()
VALUES (?, "em@ai.l", "Display Name", "Firstname", "Lastname", "1970-01-01 00:00:00", ?)"#, .into_table(Groups::Table)
) .columns(vec![Groups::GroupId, Groups::DisplayName])
.bind(name.to_string()) .values_panic(vec![id.into(), name.into()])
.bind(pass.to_string()) .to_string(SqliteQueryBuilder);
.execute(sql_pool) sqlx::query(&query).execute(sql_pool).await.unwrap();
.await }
.unwrap();
async fn insert_membership(sql_pool: &Pool, group_id: u32, user_id: &str) {
let query = Query::insert()
.into_table(Memberships::Table)
.columns(vec![Memberships::UserId, Memberships::GroupId])
.values_panic(vec![user_id.into(), group_id.into()])
.to_string(SqliteQueryBuilder);
sqlx::query(&query).execute(sql_pool).await.unwrap();
} }
#[tokio::test] #[tokio::test]
@ -317,4 +375,33 @@ mod tests {
assert_eq!(users, vec!["John", "patrick"]); assert_eq!(users, vec!["John", "patrick"]);
} }
} }
#[tokio::test]
async fn test_list_groups() {
let sql_pool = get_initialized_db().await;
insert_user(&sql_pool, "bob", "bob00").await;
insert_user(&sql_pool, "patrick", "pass").await;
insert_user(&sql_pool, "John", "Pa33w0rd!").await;
insert_group(&sql_pool, 1, "Best Group").await;
insert_group(&sql_pool, 2, "Worst Group").await;
insert_membership(&sql_pool, 1, "bob").await;
insert_membership(&sql_pool, 1, "patrick").await;
insert_membership(&sql_pool, 2, "patrick").await;
insert_membership(&sql_pool, 2, "John").await;
let config = Configuration::default();
let handler = SqlBackendHandler::new(config, sql_pool);
assert_eq!(
handler.list_groups().await.unwrap(),
vec![
Group {
display_name: "Best Group".to_string(),
users: vec!["bob".to_string(), "patrick".to_string()]
},
Group {
display_name: "Worst Group".to_string(),
users: vec!["John".to_string(), "patrick".to_string()]
}
]
);
}
} }

View File

@ -1,4 +1,3 @@
use chrono::NaiveDateTime;
use sea_query::*; use sea_query::*;
pub type Pool = sqlx::sqlite::SqlitePool; pub type Pool = sqlx::sqlite::SqlitePool;
@ -90,8 +89,7 @@ pub async fn init_table(pool: &Pool) -> sqlx::Result<()> {
.col( .col(
ColumnDef::new(Memberships::UserId) ColumnDef::new(Memberships::UserId)
.string_len(255) .string_len(255)
.not_null() .not_null(),
.primary_key(),
) )
.col(ColumnDef::new(Memberships::GroupId).integer().not_null()) .col(ColumnDef::new(Memberships::GroupId).integer().not_null())
.foreign_key( .foreign_key(
@ -121,6 +119,7 @@ pub async fn init_table(pool: &Pool) -> sqlx::Result<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use chrono::NaiveDateTime;
use sqlx::{Column, Row}; use sqlx::{Column, Row};
#[actix_rt::test] #[actix_rt::test]