use crate::core::models::team::{Team, TeamMember, TeamRole, TeamStatus};
use crate::utils::error::gateway_error::{GatewayError, Result};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::RwLock;
use uuid::Uuid;
#[async_trait]
pub trait TeamRepository: Send + Sync {
async fn create(&self, team: Team) -> Result<Team>;
async fn get(&self, id: Uuid) -> Result<Option<Team>>;
async fn get_by_name(&self, name: &str) -> Result<Option<Team>>;
async fn update(&self, team: Team) -> Result<Team>;
async fn delete(&self, id: Uuid) -> Result<()>;
async fn list(&self, offset: u32, limit: u32) -> Result<(Vec<Team>, u64)>;
async fn count(&self) -> Result<u64>;
async fn add_member(&self, member: TeamMember) -> Result<TeamMember>;
async fn get_member(&self, team_id: Uuid, user_id: Uuid) -> Result<Option<TeamMember>>;
async fn update_member_role(
&self,
team_id: Uuid,
user_id: Uuid,
role: TeamRole,
) -> Result<TeamMember>;
async fn remove_member(&self, team_id: Uuid, user_id: Uuid) -> Result<()>;
async fn list_members(&self, team_id: Uuid) -> Result<Vec<TeamMember>>;
async fn get_user_teams(&self, user_id: Uuid) -> Result<Vec<Team>>;
}
pub struct InMemoryTeamRepository {
teams: RwLock<HashMap<Uuid, Team>>,
members: RwLock<HashMap<(Uuid, Uuid), TeamMember>>,
}
impl InMemoryTeamRepository {
pub fn new() -> Self {
Self {
teams: RwLock::new(HashMap::new()),
members: RwLock::new(HashMap::new()),
}
}
}
impl Default for InMemoryTeamRepository {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl TeamRepository for InMemoryTeamRepository {
async fn create(&self, team: Team) -> Result<Team> {
let mut teams = self
.teams
.write()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
if teams.values().any(|t| t.name == team.name) {
return Err(GatewayError::Conflict(format!(
"Team with name '{}' already exists",
team.name
)));
}
let id = team.id();
teams.insert(id, team.clone());
Ok(team)
}
async fn get(&self, id: Uuid) -> Result<Option<Team>> {
let teams = self
.teams
.read()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
Ok(teams.get(&id).cloned())
}
async fn get_by_name(&self, name: &str) -> Result<Option<Team>> {
let teams = self
.teams
.read()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
Ok(teams.values().find(|t| t.name == name).cloned())
}
async fn update(&self, mut team: Team) -> Result<Team> {
let mut teams = self
.teams
.write()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
let id = team.id();
if !teams.contains_key(&id) {
return Err(GatewayError::NotFound(format!("Team {} not found", id)));
}
if teams.values().any(|t| t.name == team.name && t.id() != id) {
return Err(GatewayError::Conflict(format!(
"Team with name '{}' already exists",
team.name
)));
}
team.metadata.touch();
teams.insert(id, team.clone());
Ok(team)
}
async fn delete(&self, id: Uuid) -> Result<()> {
let mut teams = self
.teams
.write()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
if teams.remove(&id).is_none() {
return Err(GatewayError::NotFound(format!("Team {} not found", id)));
}
let mut members = self
.members
.write()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
members.retain(|(team_id, _), _| *team_id != id);
Ok(())
}
async fn list(&self, offset: u32, limit: u32) -> Result<(Vec<Team>, u64)> {
let teams = self
.teams
.read()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
let total = teams.len() as u64;
let mut team_list: Vec<Team> = teams
.values()
.filter(|t| !matches!(t.status, TeamStatus::Deleted))
.cloned()
.collect();
team_list.sort_by(|a, b| b.metadata.created_at.cmp(&a.metadata.created_at));
let paginated: Vec<Team> = team_list
.into_iter()
.skip(offset as usize)
.take(limit as usize)
.collect();
Ok((paginated, total))
}
async fn count(&self) -> Result<u64> {
let teams = self
.teams
.read()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
Ok(teams
.values()
.filter(|t| !matches!(t.status, TeamStatus::Deleted))
.count() as u64)
}
async fn add_member(&self, member: TeamMember) -> Result<TeamMember> {
{
let teams = self
.teams
.read()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
if !teams.contains_key(&member.team_id) {
return Err(GatewayError::NotFound(format!(
"Team {} not found",
member.team_id
)));
}
}
let mut members = self
.members
.write()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
let key = (member.team_id, member.user_id);
if members.contains_key(&key) {
return Err(GatewayError::Conflict(format!(
"User {} is already a member of team {}",
member.user_id, member.team_id
)));
}
members.insert(key, member.clone());
Ok(member)
}
async fn get_member(&self, team_id: Uuid, user_id: Uuid) -> Result<Option<TeamMember>> {
let members = self
.members
.read()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
Ok(members.get(&(team_id, user_id)).cloned())
}
async fn update_member_role(
&self,
team_id: Uuid,
user_id: Uuid,
role: TeamRole,
) -> Result<TeamMember> {
let mut members = self
.members
.write()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
let key = (team_id, user_id);
let member = members.get_mut(&key).ok_or_else(|| {
GatewayError::NotFound(format!("Member {} not found in team {}", user_id, team_id))
})?;
member.role = role;
member.metadata.touch();
Ok(member.clone())
}
async fn remove_member(&self, team_id: Uuid, user_id: Uuid) -> Result<()> {
let mut members = self
.members
.write()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
let key = (team_id, user_id);
if members.remove(&key).is_none() {
return Err(GatewayError::NotFound(format!(
"Member {} not found in team {}",
user_id, team_id
)));
}
Ok(())
}
async fn list_members(&self, team_id: Uuid) -> Result<Vec<TeamMember>> {
let members = self
.members
.read()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
let team_members: Vec<TeamMember> = members
.iter()
.filter(|((tid, _), _)| *tid == team_id)
.map(|(_, m)| m.clone())
.collect();
Ok(team_members)
}
async fn get_user_teams(&self, user_id: Uuid) -> Result<Vec<Team>> {
let members = self
.members
.read()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
let team_ids: Vec<Uuid> = members
.iter()
.filter(|((_, uid), _)| *uid == user_id)
.map(|((tid, _), _)| *tid)
.collect();
let teams = self
.teams
.read()
.map_err(|_| GatewayError::Internal("Lock poisoned".to_string()))?;
let user_teams: Vec<Team> = team_ids
.into_iter()
.filter_map(|tid| teams.get(&tid).cloned())
.collect();
Ok(user_teams)
}
}
#[cfg(feature = "postgres")]
pub mod postgres {
use super::*;
use crate::storage::database::Database;
use std::sync::Arc;
pub struct PostgresTeamRepository {
_db: Arc<Database>,
}
impl PostgresTeamRepository {
pub fn new(db: Arc<Database>) -> Self {
Self { _db: db }
}
}
#[async_trait]
impl TeamRepository for PostgresTeamRepository {
async fn create(&self, _team: Team) -> Result<Team> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn get(&self, _id: Uuid) -> Result<Option<Team>> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn get_by_name(&self, _name: &str) -> Result<Option<Team>> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn update(&self, _team: Team) -> Result<Team> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn delete(&self, _id: Uuid) -> Result<()> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn list(&self, _offset: u32, _limit: u32) -> Result<(Vec<Team>, u64)> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn count(&self) -> Result<u64> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn add_member(&self, _member: TeamMember) -> Result<TeamMember> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn get_member(&self, _team_id: Uuid, _user_id: Uuid) -> Result<Option<TeamMember>> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn update_member_role(
&self,
_team_id: Uuid,
_user_id: Uuid,
_role: TeamRole,
) -> Result<TeamMember> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn remove_member(&self, _team_id: Uuid, _user_id: Uuid) -> Result<()> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn list_members(&self, _team_id: Uuid) -> Result<Vec<TeamMember>> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
async fn get_user_teams(&self, _user_id: Uuid) -> Result<Vec<Team>> {
Err(GatewayError::NotImplemented(
"PostgreSQL team storage not yet implemented".to_string(),
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_team() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("test-team".to_string(), Some("Test Team".to_string()));
let created = repo.create(team.clone()).await.unwrap();
assert_eq!(created.name, "test-team");
}
#[tokio::test]
async fn test_get_team() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("test-team".to_string(), None);
let id = team.id();
repo.create(team).await.unwrap();
let fetched = repo.get(id).await.unwrap();
assert!(fetched.is_some());
assert_eq!(fetched.unwrap().name, "test-team");
}
#[tokio::test]
async fn test_get_team_not_found() {
let repo = InMemoryTeamRepository::new();
let result = repo.get(Uuid::new_v4()).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_get_by_name() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("unique-name".to_string(), None);
repo.create(team).await.unwrap();
let fetched = repo.get_by_name("unique-name").await.unwrap();
assert!(fetched.is_some());
assert_eq!(fetched.unwrap().name, "unique-name");
}
#[tokio::test]
async fn test_update_team() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("original".to_string(), None);
let id = team.id();
repo.create(team).await.unwrap();
let mut updated_team = repo.get(id).await.unwrap().unwrap();
updated_team.description = Some("Updated description".to_string());
let result = repo.update(updated_team).await.unwrap();
assert_eq!(result.description, Some("Updated description".to_string()));
}
#[tokio::test]
async fn test_delete_team() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("to-delete".to_string(), None);
let id = team.id();
repo.create(team).await.unwrap();
repo.delete(id).await.unwrap();
let fetched = repo.get(id).await.unwrap();
assert!(fetched.is_none());
}
#[tokio::test]
async fn test_list_teams() {
let repo = InMemoryTeamRepository::new();
for i in 0..5 {
let team = Team::new(format!("team-{}", i), None);
repo.create(team).await.unwrap();
}
let (teams, total) = repo.list(0, 10).await.unwrap();
assert_eq!(teams.len(), 5);
assert_eq!(total, 5);
}
#[tokio::test]
async fn test_list_teams_pagination() {
let repo = InMemoryTeamRepository::new();
for i in 0..10 {
let team = Team::new(format!("team-{}", i), None);
repo.create(team).await.unwrap();
}
let (teams, total) = repo.list(0, 5).await.unwrap();
assert_eq!(teams.len(), 5);
assert_eq!(total, 10);
let (teams2, _) = repo.list(5, 5).await.unwrap();
assert_eq!(teams2.len(), 5);
}
#[tokio::test]
async fn test_duplicate_team_name() {
let repo = InMemoryTeamRepository::new();
let team1 = Team::new("duplicate".to_string(), None);
let team2 = Team::new("duplicate".to_string(), None);
repo.create(team1).await.unwrap();
let result = repo.create(team2).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_add_member() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("team".to_string(), None);
let team_id = team.id();
repo.create(team).await.unwrap();
let user_id = Uuid::new_v4();
let member = TeamMember::new(team_id, user_id, TeamRole::Member, None);
let created = repo.add_member(member).await.unwrap();
assert_eq!(created.user_id, user_id);
assert!(matches!(created.role, TeamRole::Member));
}
#[tokio::test]
async fn test_get_member() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("team".to_string(), None);
let team_id = team.id();
repo.create(team).await.unwrap();
let user_id = Uuid::new_v4();
let member = TeamMember::new(team_id, user_id, TeamRole::Admin, None);
repo.add_member(member).await.unwrap();
let fetched = repo.get_member(team_id, user_id).await.unwrap();
assert!(fetched.is_some());
assert!(matches!(fetched.unwrap().role, TeamRole::Admin));
}
#[tokio::test]
async fn test_update_member_role() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("team".to_string(), None);
let team_id = team.id();
repo.create(team).await.unwrap();
let user_id = Uuid::new_v4();
let member = TeamMember::new(team_id, user_id, TeamRole::Member, None);
repo.add_member(member).await.unwrap();
let updated = repo
.update_member_role(team_id, user_id, TeamRole::Admin)
.await
.unwrap();
assert!(matches!(updated.role, TeamRole::Admin));
}
#[tokio::test]
async fn test_remove_member() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("team".to_string(), None);
let team_id = team.id();
repo.create(team).await.unwrap();
let user_id = Uuid::new_v4();
let member = TeamMember::new(team_id, user_id, TeamRole::Member, None);
repo.add_member(member).await.unwrap();
repo.remove_member(team_id, user_id).await.unwrap();
let fetched = repo.get_member(team_id, user_id).await.unwrap();
assert!(fetched.is_none());
}
#[tokio::test]
async fn test_list_members() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("team".to_string(), None);
let team_id = team.id();
repo.create(team).await.unwrap();
for _ in 0..3 {
let member = TeamMember::new(team_id, Uuid::new_v4(), TeamRole::Member, None);
repo.add_member(member).await.unwrap();
}
let members = repo.list_members(team_id).await.unwrap();
assert_eq!(members.len(), 3);
}
#[tokio::test]
async fn test_get_user_teams() {
let repo = InMemoryTeamRepository::new();
let user_id = Uuid::new_v4();
for i in 0..3 {
let team = Team::new(format!("team-{}", i), None);
let team_id = team.id();
repo.create(team).await.unwrap();
let member = TeamMember::new(team_id, user_id, TeamRole::Member, None);
repo.add_member(member).await.unwrap();
}
let teams = repo.get_user_teams(user_id).await.unwrap();
assert_eq!(teams.len(), 3);
}
#[tokio::test]
async fn test_add_member_to_nonexistent_team() {
let repo = InMemoryTeamRepository::new();
let member = TeamMember::new(Uuid::new_v4(), Uuid::new_v4(), TeamRole::Member, None);
let result = repo.add_member(member).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_duplicate_member() {
let repo = InMemoryTeamRepository::new();
let team = Team::new("team".to_string(), None);
let team_id = team.id();
repo.create(team).await.unwrap();
let user_id = Uuid::new_v4();
let member1 = TeamMember::new(team_id, user_id, TeamRole::Member, None);
let member2 = TeamMember::new(team_id, user_id, TeamRole::Admin, None);
repo.add_member(member1).await.unwrap();
let result = repo.add_member(member2).await;
assert!(result.is_err());
}
}