use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "user_role", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum UserRole {
Admin,
Editor,
Viewer,
}
impl UserRole {
#[must_use]
pub const fn is_admin(&self) -> bool {
matches!(self, Self::Admin)
}
#[must_use]
pub const fn can_edit(&self) -> bool {
matches!(self, Self::Admin | Self::Editor)
}
#[must_use]
pub const fn can_view(&self) -> bool {
true }
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct User {
pub id: Uuid,
pub username: String,
pub email: String,
#[serde(skip_serializing)]
pub password_hash: String,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub is_active: bool,
}
impl User {
#[must_use]
pub fn new(username: String, email: String, password_hash: String) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
username,
email,
password_hash,
display_name: None,
avatar_url: None,
created_at: now,
updated_at: now,
is_active: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct TeamWorkspace {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub owner_id: Uuid,
pub config: serde_json::Value,
pub version: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub is_archived: bool,
}
impl TeamWorkspace {
#[must_use]
pub fn new(name: String, owner_id: Uuid) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
name,
description: None,
owner_id,
config: serde_json::json!({}),
version: 1,
created_at: now,
updated_at: now,
is_archived: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct WorkspaceMember {
pub id: Uuid,
pub workspace_id: Uuid,
pub user_id: Uuid,
pub role: UserRole,
pub joined_at: DateTime<Utc>,
pub last_activity: DateTime<Utc>,
}
impl WorkspaceMember {
#[must_use]
pub fn new(workspace_id: Uuid, user_id: Uuid, role: UserRole) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
workspace_id,
user_id,
role,
joined_at: now,
last_activity: now,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct WorkspaceInvitation {
pub id: Uuid,
pub workspace_id: Uuid,
pub email: String,
pub role: UserRole,
pub invited_by: Uuid,
pub token: String,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub accepted: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveSession {
pub user_id: Uuid,
pub workspace_id: Uuid,
pub session_id: Uuid,
pub connected_at: DateTime<Utc>,
pub last_activity: DateTime<Utc>,
pub cursor: Option<CursorPosition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CursorPosition {
pub resource: String,
pub line: Option<u32>,
pub column: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct WorkspaceFork {
pub id: Uuid,
pub source_workspace_id: Uuid,
pub forked_workspace_id: Uuid,
pub forked_at: DateTime<Utc>,
pub forked_by: Uuid,
pub fork_point_commit_id: Option<Uuid>,
}
impl WorkspaceFork {
#[must_use]
pub fn new(
source_workspace_id: Uuid,
forked_workspace_id: Uuid,
forked_by: Uuid,
fork_point_commit_id: Option<Uuid>,
) -> Self {
Self {
id: Uuid::new_v4(),
source_workspace_id,
forked_workspace_id,
forked_by,
fork_point_commit_id,
forked_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "merge_status", rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum MergeStatus {
Pending,
InProgress,
Completed,
Conflict,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct WorkspaceMerge {
pub id: Uuid,
pub source_workspace_id: Uuid,
pub target_workspace_id: Uuid,
pub base_commit_id: Uuid,
pub source_commit_id: Uuid,
pub target_commit_id: Uuid,
pub merge_commit_id: Option<Uuid>,
pub status: MergeStatus,
pub conflict_data: Option<serde_json::Value>,
pub merged_by: Option<Uuid>,
pub merged_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
impl WorkspaceMerge {
#[must_use]
pub fn new(
source_workspace_id: Uuid,
target_workspace_id: Uuid,
base_commit_id: Uuid,
source_commit_id: Uuid,
target_commit_id: Uuid,
) -> Self {
Self {
id: Uuid::new_v4(),
source_workspace_id,
target_workspace_id,
base_commit_id,
source_commit_id,
target_commit_id,
merge_commit_id: None,
status: MergeStatus::Pending,
conflict_data: None,
merged_by: None,
merged_at: None,
created_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MergeConflict {
pub path: String,
pub base_value: Option<serde_json::Value>,
pub source_value: Option<serde_json::Value>,
pub target_value: Option<serde_json::Value>,
pub conflict_type: ConflictType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConflictType {
Modified,
DeletedModified,
BothAdded,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_role_permissions() {
assert!(UserRole::Admin.is_admin());
assert!(UserRole::Admin.can_edit());
assert!(UserRole::Admin.can_view());
assert!(!UserRole::Editor.is_admin());
assert!(UserRole::Editor.can_edit());
assert!(UserRole::Editor.can_view());
assert!(!UserRole::Viewer.is_admin());
assert!(!UserRole::Viewer.can_edit());
assert!(UserRole::Viewer.can_view());
}
#[test]
fn test_user_role_equality() {
assert_eq!(UserRole::Admin, UserRole::Admin);
assert_ne!(UserRole::Admin, UserRole::Editor);
assert_ne!(UserRole::Editor, UserRole::Viewer);
}
#[test]
fn test_user_role_clone() {
let role = UserRole::Editor;
let cloned = role;
assert_eq!(cloned, UserRole::Editor);
}
#[test]
fn test_user_role_serialization() {
let role = UserRole::Admin;
let json = serde_json::to_string(&role).unwrap();
assert_eq!(json, r#""admin""#);
}
#[test]
fn test_user_role_deserialization() {
let role: UserRole = serde_json::from_str(r#""viewer""#).unwrap();
assert_eq!(role, UserRole::Viewer);
}
#[test]
fn test_user_creation() {
let user = User::new(
"testuser".to_string(),
"test@example.com".to_string(),
"hashed_password".to_string(),
);
assert_eq!(user.username, "testuser");
assert_eq!(user.email, "test@example.com");
assert!(user.is_active);
}
#[test]
fn test_user_new_defaults() {
let user = User::new("user".to_string(), "user@test.com".to_string(), "hash".to_string());
assert!(user.display_name.is_none());
assert!(user.avatar_url.is_none());
assert!(user.is_active);
}
#[test]
fn test_user_has_unique_id() {
let user1 = User::new("u1".to_string(), "u1@test.com".to_string(), "h1".to_string());
let user2 = User::new("u2".to_string(), "u2@test.com".to_string(), "h2".to_string());
assert_ne!(user1.id, user2.id);
}
#[test]
fn test_user_serialization_skips_password() {
let user = User::new(
"testuser".to_string(),
"test@example.com".to_string(),
"secret_hash".to_string(),
);
let json = serde_json::to_string(&user).unwrap();
assert!(!json.contains("secret_hash"));
assert!(!json.contains("password_hash"));
}
#[test]
fn test_workspace_creation() {
let owner_id = Uuid::new_v4();
let workspace = TeamWorkspace::new("Test Workspace".to_string(), owner_id);
assert_eq!(workspace.name, "Test Workspace");
assert_eq!(workspace.owner_id, owner_id);
assert_eq!(workspace.version, 1);
assert!(!workspace.is_archived);
}
#[test]
fn test_workspace_defaults() {
let owner_id = Uuid::new_v4();
let workspace = TeamWorkspace::new("Test".to_string(), owner_id);
assert!(workspace.description.is_none());
assert!(!workspace.is_archived);
assert_eq!(workspace.config, serde_json::json!({}));
}
#[test]
fn test_workspace_has_unique_id() {
let owner_id = Uuid::new_v4();
let ws1 = TeamWorkspace::new("WS1".to_string(), owner_id);
let ws2 = TeamWorkspace::new("WS2".to_string(), owner_id);
assert_ne!(ws1.id, ws2.id);
}
#[test]
fn test_workspace_member_creation() {
let workspace_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let member = WorkspaceMember::new(workspace_id, user_id, UserRole::Editor);
assert_eq!(member.workspace_id, workspace_id);
assert_eq!(member.user_id, user_id);
assert_eq!(member.role, UserRole::Editor);
}
#[test]
fn test_workspace_member_admin() {
let workspace_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let member = WorkspaceMember::new(workspace_id, user_id, UserRole::Admin);
assert!(member.role.is_admin());
assert!(member.role.can_edit());
}
#[test]
fn test_workspace_member_viewer() {
let workspace_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let member = WorkspaceMember::new(workspace_id, user_id, UserRole::Viewer);
assert!(!member.role.is_admin());
assert!(!member.role.can_edit());
assert!(member.role.can_view());
}
#[test]
fn test_workspace_fork_creation() {
let source_id = Uuid::new_v4();
let forked_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let commit_id = Uuid::new_v4();
let fork = WorkspaceFork::new(source_id, forked_id, user_id, Some(commit_id));
assert_eq!(fork.source_workspace_id, source_id);
assert_eq!(fork.forked_workspace_id, forked_id);
assert_eq!(fork.forked_by, user_id);
assert_eq!(fork.fork_point_commit_id, Some(commit_id));
}
#[test]
fn test_workspace_fork_without_commit() {
let source_id = Uuid::new_v4();
let forked_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let fork = WorkspaceFork::new(source_id, forked_id, user_id, None);
assert!(fork.fork_point_commit_id.is_none());
}
#[test]
fn test_workspace_fork_has_unique_id() {
let fork1 = WorkspaceFork::new(Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4(), None);
let fork2 = WorkspaceFork::new(Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4(), None);
assert_ne!(fork1.id, fork2.id);
}
#[test]
fn test_merge_status_equality() {
assert_eq!(MergeStatus::Pending, MergeStatus::Pending);
assert_ne!(MergeStatus::Pending, MergeStatus::Completed);
}
#[test]
fn test_merge_status_serialization() {
let status = MergeStatus::InProgress;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, r#""in_progress""#);
}
#[test]
fn test_merge_status_deserialization() {
let status: MergeStatus = serde_json::from_str(r#""conflict""#).unwrap();
assert_eq!(status, MergeStatus::Conflict);
}
#[test]
fn test_merge_status_all_variants() {
let variants = vec![
MergeStatus::Pending,
MergeStatus::InProgress,
MergeStatus::Completed,
MergeStatus::Conflict,
MergeStatus::Cancelled,
];
for status in variants {
let json = serde_json::to_string(&status).unwrap();
let deserialized: MergeStatus = serde_json::from_str(&json).unwrap();
assert_eq!(status, deserialized);
}
}
#[test]
fn test_workspace_merge_creation() {
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let base_id = Uuid::new_v4();
let source_commit = Uuid::new_v4();
let target_commit = Uuid::new_v4();
let merge =
WorkspaceMerge::new(source_id, target_id, base_id, source_commit, target_commit);
assert_eq!(merge.source_workspace_id, source_id);
assert_eq!(merge.target_workspace_id, target_id);
assert_eq!(merge.base_commit_id, base_id);
assert_eq!(merge.status, MergeStatus::Pending);
assert!(merge.merge_commit_id.is_none());
assert!(merge.merged_by.is_none());
assert!(merge.merged_at.is_none());
}
#[test]
fn test_workspace_merge_default_status() {
let merge = WorkspaceMerge::new(
Uuid::new_v4(),
Uuid::new_v4(),
Uuid::new_v4(),
Uuid::new_v4(),
Uuid::new_v4(),
);
assert_eq!(merge.status, MergeStatus::Pending);
assert!(merge.conflict_data.is_none());
}
#[test]
fn test_conflict_type_equality() {
assert_eq!(ConflictType::Modified, ConflictType::Modified);
assert_ne!(ConflictType::Modified, ConflictType::BothAdded);
}
#[test]
fn test_conflict_type_serialization() {
let conflict = ConflictType::DeletedModified;
let json = serde_json::to_string(&conflict).unwrap();
assert_eq!(json, r#""deletedmodified""#);
}
#[test]
fn test_conflict_type_all_variants() {
let variants = vec![
ConflictType::Modified,
ConflictType::DeletedModified,
ConflictType::BothAdded,
];
for conflict_type in variants {
let json = serde_json::to_string(&conflict_type).unwrap();
let deserialized: ConflictType = serde_json::from_str(&json).unwrap();
assert_eq!(conflict_type, deserialized);
}
}
#[test]
fn test_merge_conflict_creation() {
let conflict = MergeConflict {
path: "/routes/users".to_string(),
base_value: Some(serde_json::json!({"method": "GET"})),
source_value: Some(serde_json::json!({"method": "POST"})),
target_value: Some(serde_json::json!({"method": "PUT"})),
conflict_type: ConflictType::Modified,
};
assert_eq!(conflict.path, "/routes/users");
assert_eq!(conflict.conflict_type, ConflictType::Modified);
}
#[test]
fn test_merge_conflict_with_none_values() {
let conflict = MergeConflict {
path: "/routes/new".to_string(),
base_value: None,
source_value: Some(serde_json::json!({"method": "GET"})),
target_value: Some(serde_json::json!({"method": "POST"})),
conflict_type: ConflictType::BothAdded,
};
assert!(conflict.base_value.is_none());
assert!(conflict.source_value.is_some());
}
#[test]
fn test_cursor_position_creation() {
let cursor = CursorPosition {
resource: "routes.yaml".to_string(),
line: Some(42),
column: Some(10),
};
assert_eq!(cursor.resource, "routes.yaml");
assert_eq!(cursor.line, Some(42));
assert_eq!(cursor.column, Some(10));
}
#[test]
fn test_cursor_position_without_line_column() {
let cursor = CursorPosition {
resource: "config.json".to_string(),
line: None,
column: None,
};
assert!(cursor.line.is_none());
assert!(cursor.column.is_none());
}
#[test]
fn test_cursor_position_serialization() {
let cursor = CursorPosition {
resource: "test.yaml".to_string(),
line: Some(1),
column: Some(5),
};
let json = serde_json::to_string(&cursor).unwrap();
let deserialized: CursorPosition = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.resource, "test.yaml");
assert_eq!(deserialized.line, Some(1));
}
#[test]
fn test_active_session_creation() {
let user_id = Uuid::new_v4();
let workspace_id = Uuid::new_v4();
let session_id = Uuid::new_v4();
let now = Utc::now();
let session = ActiveSession {
user_id,
workspace_id,
session_id,
connected_at: now,
last_activity: now,
cursor: None,
};
assert_eq!(session.user_id, user_id);
assert_eq!(session.workspace_id, workspace_id);
assert!(session.cursor.is_none());
}
#[test]
fn test_active_session_with_cursor() {
let user_id = Uuid::new_v4();
let workspace_id = Uuid::new_v4();
let session_id = Uuid::new_v4();
let now = Utc::now();
let cursor = CursorPosition {
resource: "routes.yaml".to_string(),
line: Some(10),
column: Some(5),
};
let session = ActiveSession {
user_id,
workspace_id,
session_id,
connected_at: now,
last_activity: now,
cursor: Some(cursor),
};
assert!(session.cursor.is_some());
assert_eq!(session.cursor.as_ref().unwrap().resource, "routes.yaml");
}
#[test]
fn test_active_session_serialization() {
let session = ActiveSession {
user_id: Uuid::new_v4(),
workspace_id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
connected_at: Utc::now(),
last_activity: Utc::now(),
cursor: None,
};
let json = serde_json::to_string(&session).unwrap();
let deserialized: ActiveSession = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.user_id, session.user_id);
}
}