1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
9#[sqlx(type_name = "user_role", rename_all = "lowercase")]
10#[serde(rename_all = "lowercase")]
11pub enum UserRole {
12 Admin,
14 Editor,
16 Viewer,
18}
19
20impl UserRole {
21 #[must_use]
23 pub const fn is_admin(&self) -> bool {
24 matches!(self, Self::Admin)
25 }
26
27 #[must_use]
29 pub const fn can_edit(&self) -> bool {
30 matches!(self, Self::Admin | Self::Editor)
31 }
32
33 #[must_use]
35 pub const fn can_view(&self) -> bool {
36 true }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
42pub struct User {
43 pub id: Uuid,
45 pub username: String,
47 pub email: String,
49 #[serde(skip_serializing)]
51 pub password_hash: String,
52 pub display_name: Option<String>,
54 pub avatar_url: Option<String>,
56 pub created_at: DateTime<Utc>,
58 pub updated_at: DateTime<Utc>,
60 pub is_active: bool,
62}
63
64impl User {
65 #[must_use]
67 pub fn new(username: String, email: String, password_hash: String) -> Self {
68 let now = Utc::now();
69 Self {
70 id: Uuid::new_v4(),
71 username,
72 email,
73 password_hash,
74 display_name: None,
75 avatar_url: None,
76 created_at: now,
77 updated_at: now,
78 is_active: true,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
85pub struct TeamWorkspace {
86 pub id: Uuid,
88 pub name: String,
90 pub description: Option<String>,
92 pub owner_id: Uuid,
94 pub config: serde_json::Value,
96 pub version: i64,
98 pub created_at: DateTime<Utc>,
100 pub updated_at: DateTime<Utc>,
102 pub is_archived: bool,
104}
105
106impl TeamWorkspace {
107 #[must_use]
109 pub fn new(name: String, owner_id: Uuid) -> Self {
110 let now = Utc::now();
111 Self {
112 id: Uuid::new_v4(),
113 name,
114 description: None,
115 owner_id,
116 config: serde_json::json!({}),
117 version: 1,
118 created_at: now,
119 updated_at: now,
120 is_archived: false,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
127pub struct WorkspaceMember {
128 pub id: Uuid,
130 pub workspace_id: Uuid,
132 pub user_id: Uuid,
134 pub role: UserRole,
136 pub joined_at: DateTime<Utc>,
138 pub last_activity: DateTime<Utc>,
140}
141
142impl WorkspaceMember {
143 #[must_use]
145 pub fn new(workspace_id: Uuid, user_id: Uuid, role: UserRole) -> Self {
146 let now = Utc::now();
147 Self {
148 id: Uuid::new_v4(),
149 workspace_id,
150 user_id,
151 role,
152 joined_at: now,
153 last_activity: now,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
160pub struct WorkspaceInvitation {
161 pub id: Uuid,
163 pub workspace_id: Uuid,
165 pub email: String,
167 pub role: UserRole,
169 pub invited_by: Uuid,
171 pub token: String,
173 pub expires_at: DateTime<Utc>,
175 pub created_at: DateTime<Utc>,
177 pub accepted: bool,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ActiveSession {
184 pub user_id: Uuid,
186 pub workspace_id: Uuid,
188 pub session_id: Uuid,
190 pub connected_at: DateTime<Utc>,
192 pub last_activity: DateTime<Utc>,
194 pub cursor: Option<CursorPosition>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct CursorPosition {
201 pub resource: String,
203 pub line: Option<u32>,
205 pub column: Option<u32>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
211pub struct WorkspaceFork {
212 pub id: Uuid,
214 pub source_workspace_id: Uuid,
216 pub forked_workspace_id: Uuid,
218 pub forked_at: DateTime<Utc>,
220 pub forked_by: Uuid,
222 pub fork_point_commit_id: Option<Uuid>,
224}
225
226impl WorkspaceFork {
227 #[must_use]
229 pub fn new(
230 source_workspace_id: Uuid,
231 forked_workspace_id: Uuid,
232 forked_by: Uuid,
233 fork_point_commit_id: Option<Uuid>,
234 ) -> Self {
235 Self {
236 id: Uuid::new_v4(),
237 source_workspace_id,
238 forked_workspace_id,
239 forked_by,
240 fork_point_commit_id,
241 forked_at: Utc::now(),
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
248#[sqlx(type_name = "merge_status", rename_all = "lowercase")]
249#[serde(rename_all = "lowercase")]
250pub enum MergeStatus {
251 Pending,
253 InProgress,
255 Completed,
257 Conflict,
259 Cancelled,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
265pub struct WorkspaceMerge {
266 pub id: Uuid,
268 pub source_workspace_id: Uuid,
270 pub target_workspace_id: Uuid,
272 pub base_commit_id: Uuid,
274 pub source_commit_id: Uuid,
276 pub target_commit_id: Uuid,
278 pub merge_commit_id: Option<Uuid>,
280 pub status: MergeStatus,
282 pub conflict_data: Option<serde_json::Value>,
284 pub merged_by: Option<Uuid>,
286 pub merged_at: Option<DateTime<Utc>>,
288 pub created_at: DateTime<Utc>,
290}
291
292impl WorkspaceMerge {
293 #[must_use]
295 pub fn new(
296 source_workspace_id: Uuid,
297 target_workspace_id: Uuid,
298 base_commit_id: Uuid,
299 source_commit_id: Uuid,
300 target_commit_id: Uuid,
301 ) -> Self {
302 Self {
303 id: Uuid::new_v4(),
304 source_workspace_id,
305 target_workspace_id,
306 base_commit_id,
307 source_commit_id,
308 target_commit_id,
309 merge_commit_id: None,
310 status: MergeStatus::Pending,
311 conflict_data: None,
312 merged_by: None,
313 merged_at: None,
314 created_at: Utc::now(),
315 }
316 }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct MergeConflict {
322 pub path: String,
324 pub base_value: Option<serde_json::Value>,
326 pub source_value: Option<serde_json::Value>,
328 pub target_value: Option<serde_json::Value>,
330 pub conflict_type: ConflictType,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(rename_all = "lowercase")]
337pub enum ConflictType {
338 Modified,
340 DeletedModified,
342 BothAdded,
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
353 fn test_user_role_permissions() {
354 assert!(UserRole::Admin.is_admin());
355 assert!(UserRole::Admin.can_edit());
356 assert!(UserRole::Admin.can_view());
357
358 assert!(!UserRole::Editor.is_admin());
359 assert!(UserRole::Editor.can_edit());
360 assert!(UserRole::Editor.can_view());
361
362 assert!(!UserRole::Viewer.is_admin());
363 assert!(!UserRole::Viewer.can_edit());
364 assert!(UserRole::Viewer.can_view());
365 }
366
367 #[test]
368 fn test_user_role_equality() {
369 assert_eq!(UserRole::Admin, UserRole::Admin);
370 assert_ne!(UserRole::Admin, UserRole::Editor);
371 assert_ne!(UserRole::Editor, UserRole::Viewer);
372 }
373
374 #[test]
375 fn test_user_role_clone() {
376 let role = UserRole::Editor;
377 let cloned = role;
378 assert_eq!(cloned, UserRole::Editor);
379 }
380
381 #[test]
382 fn test_user_role_serialization() {
383 let role = UserRole::Admin;
384 let json = serde_json::to_string(&role).unwrap();
385 assert_eq!(json, r#""admin""#);
386 }
387
388 #[test]
389 fn test_user_role_deserialization() {
390 let role: UserRole = serde_json::from_str(r#""viewer""#).unwrap();
391 assert_eq!(role, UserRole::Viewer);
392 }
393
394 #[test]
397 fn test_user_creation() {
398 let user = User::new(
399 "testuser".to_string(),
400 "test@example.com".to_string(),
401 "hashed_password".to_string(),
402 );
403
404 assert_eq!(user.username, "testuser");
405 assert_eq!(user.email, "test@example.com");
406 assert!(user.is_active);
407 }
408
409 #[test]
410 fn test_user_new_defaults() {
411 let user = User::new("user".to_string(), "user@test.com".to_string(), "hash".to_string());
412
413 assert!(user.display_name.is_none());
414 assert!(user.avatar_url.is_none());
415 assert!(user.is_active);
416 }
417
418 #[test]
419 fn test_user_has_unique_id() {
420 let user1 = User::new("u1".to_string(), "u1@test.com".to_string(), "h1".to_string());
421 let user2 = User::new("u2".to_string(), "u2@test.com".to_string(), "h2".to_string());
422
423 assert_ne!(user1.id, user2.id);
424 }
425
426 #[test]
427 fn test_user_serialization_skips_password() {
428 let user = User::new(
429 "testuser".to_string(),
430 "test@example.com".to_string(),
431 "secret_hash".to_string(),
432 );
433
434 let json = serde_json::to_string(&user).unwrap();
435 assert!(!json.contains("secret_hash"));
436 assert!(!json.contains("password_hash"));
437 }
438
439 #[test]
442 fn test_workspace_creation() {
443 let owner_id = Uuid::new_v4();
444 let workspace = TeamWorkspace::new("Test Workspace".to_string(), owner_id);
445
446 assert_eq!(workspace.name, "Test Workspace");
447 assert_eq!(workspace.owner_id, owner_id);
448 assert_eq!(workspace.version, 1);
449 assert!(!workspace.is_archived);
450 }
451
452 #[test]
453 fn test_workspace_defaults() {
454 let owner_id = Uuid::new_v4();
455 let workspace = TeamWorkspace::new("Test".to_string(), owner_id);
456
457 assert!(workspace.description.is_none());
458 assert!(!workspace.is_archived);
459 assert_eq!(workspace.config, serde_json::json!({}));
460 }
461
462 #[test]
463 fn test_workspace_has_unique_id() {
464 let owner_id = Uuid::new_v4();
465 let ws1 = TeamWorkspace::new("WS1".to_string(), owner_id);
466 let ws2 = TeamWorkspace::new("WS2".to_string(), owner_id);
467
468 assert_ne!(ws1.id, ws2.id);
469 }
470
471 #[test]
474 fn test_workspace_member_creation() {
475 let workspace_id = Uuid::new_v4();
476 let user_id = Uuid::new_v4();
477 let member = WorkspaceMember::new(workspace_id, user_id, UserRole::Editor);
478
479 assert_eq!(member.workspace_id, workspace_id);
480 assert_eq!(member.user_id, user_id);
481 assert_eq!(member.role, UserRole::Editor);
482 }
483
484 #[test]
485 fn test_workspace_member_admin() {
486 let workspace_id = Uuid::new_v4();
487 let user_id = Uuid::new_v4();
488 let member = WorkspaceMember::new(workspace_id, user_id, UserRole::Admin);
489
490 assert!(member.role.is_admin());
491 assert!(member.role.can_edit());
492 }
493
494 #[test]
495 fn test_workspace_member_viewer() {
496 let workspace_id = Uuid::new_v4();
497 let user_id = Uuid::new_v4();
498 let member = WorkspaceMember::new(workspace_id, user_id, UserRole::Viewer);
499
500 assert!(!member.role.is_admin());
501 assert!(!member.role.can_edit());
502 assert!(member.role.can_view());
503 }
504
505 #[test]
508 fn test_workspace_fork_creation() {
509 let source_id = Uuid::new_v4();
510 let forked_id = Uuid::new_v4();
511 let user_id = Uuid::new_v4();
512 let commit_id = Uuid::new_v4();
513
514 let fork = WorkspaceFork::new(source_id, forked_id, user_id, Some(commit_id));
515
516 assert_eq!(fork.source_workspace_id, source_id);
517 assert_eq!(fork.forked_workspace_id, forked_id);
518 assert_eq!(fork.forked_by, user_id);
519 assert_eq!(fork.fork_point_commit_id, Some(commit_id));
520 }
521
522 #[test]
523 fn test_workspace_fork_without_commit() {
524 let source_id = Uuid::new_v4();
525 let forked_id = Uuid::new_v4();
526 let user_id = Uuid::new_v4();
527
528 let fork = WorkspaceFork::new(source_id, forked_id, user_id, None);
529
530 assert!(fork.fork_point_commit_id.is_none());
531 }
532
533 #[test]
534 fn test_workspace_fork_has_unique_id() {
535 let fork1 = WorkspaceFork::new(Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4(), None);
536 let fork2 = WorkspaceFork::new(Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4(), None);
537
538 assert_ne!(fork1.id, fork2.id);
539 }
540
541 #[test]
544 fn test_merge_status_equality() {
545 assert_eq!(MergeStatus::Pending, MergeStatus::Pending);
546 assert_ne!(MergeStatus::Pending, MergeStatus::Completed);
547 }
548
549 #[test]
550 fn test_merge_status_serialization() {
551 let status = MergeStatus::InProgress;
552 let json = serde_json::to_string(&status).unwrap();
553 assert_eq!(json, r#""inprogress""#);
554 }
555
556 #[test]
557 fn test_merge_status_deserialization() {
558 let status: MergeStatus = serde_json::from_str(r#""conflict""#).unwrap();
559 assert_eq!(status, MergeStatus::Conflict);
560 }
561
562 #[test]
563 fn test_merge_status_all_variants() {
564 let variants = vec![
565 MergeStatus::Pending,
566 MergeStatus::InProgress,
567 MergeStatus::Completed,
568 MergeStatus::Conflict,
569 MergeStatus::Cancelled,
570 ];
571
572 for status in variants {
573 let json = serde_json::to_string(&status).unwrap();
574 let deserialized: MergeStatus = serde_json::from_str(&json).unwrap();
575 assert_eq!(status, deserialized);
576 }
577 }
578
579 #[test]
582 fn test_workspace_merge_creation() {
583 let source_id = Uuid::new_v4();
584 let target_id = Uuid::new_v4();
585 let base_id = Uuid::new_v4();
586 let source_commit = Uuid::new_v4();
587 let target_commit = Uuid::new_v4();
588
589 let merge =
590 WorkspaceMerge::new(source_id, target_id, base_id, source_commit, target_commit);
591
592 assert_eq!(merge.source_workspace_id, source_id);
593 assert_eq!(merge.target_workspace_id, target_id);
594 assert_eq!(merge.base_commit_id, base_id);
595 assert_eq!(merge.status, MergeStatus::Pending);
596 assert!(merge.merge_commit_id.is_none());
597 assert!(merge.merged_by.is_none());
598 assert!(merge.merged_at.is_none());
599 }
600
601 #[test]
602 fn test_workspace_merge_default_status() {
603 let merge = WorkspaceMerge::new(
604 Uuid::new_v4(),
605 Uuid::new_v4(),
606 Uuid::new_v4(),
607 Uuid::new_v4(),
608 Uuid::new_v4(),
609 );
610
611 assert_eq!(merge.status, MergeStatus::Pending);
612 assert!(merge.conflict_data.is_none());
613 }
614
615 #[test]
618 fn test_conflict_type_equality() {
619 assert_eq!(ConflictType::Modified, ConflictType::Modified);
620 assert_ne!(ConflictType::Modified, ConflictType::BothAdded);
621 }
622
623 #[test]
624 fn test_conflict_type_serialization() {
625 let conflict = ConflictType::DeletedModified;
626 let json = serde_json::to_string(&conflict).unwrap();
627 assert_eq!(json, r#""deletedmodified""#);
628 }
629
630 #[test]
631 fn test_conflict_type_all_variants() {
632 let variants = vec![
633 ConflictType::Modified,
634 ConflictType::DeletedModified,
635 ConflictType::BothAdded,
636 ];
637
638 for conflict_type in variants {
639 let json = serde_json::to_string(&conflict_type).unwrap();
640 let deserialized: ConflictType = serde_json::from_str(&json).unwrap();
641 assert_eq!(conflict_type, deserialized);
642 }
643 }
644
645 #[test]
648 fn test_merge_conflict_creation() {
649 let conflict = MergeConflict {
650 path: "/routes/users".to_string(),
651 base_value: Some(serde_json::json!({"method": "GET"})),
652 source_value: Some(serde_json::json!({"method": "POST"})),
653 target_value: Some(serde_json::json!({"method": "PUT"})),
654 conflict_type: ConflictType::Modified,
655 };
656
657 assert_eq!(conflict.path, "/routes/users");
658 assert_eq!(conflict.conflict_type, ConflictType::Modified);
659 }
660
661 #[test]
662 fn test_merge_conflict_with_none_values() {
663 let conflict = MergeConflict {
664 path: "/routes/new".to_string(),
665 base_value: None,
666 source_value: Some(serde_json::json!({"method": "GET"})),
667 target_value: Some(serde_json::json!({"method": "POST"})),
668 conflict_type: ConflictType::BothAdded,
669 };
670
671 assert!(conflict.base_value.is_none());
672 assert!(conflict.source_value.is_some());
673 }
674
675 #[test]
678 fn test_cursor_position_creation() {
679 let cursor = CursorPosition {
680 resource: "routes.yaml".to_string(),
681 line: Some(42),
682 column: Some(10),
683 };
684
685 assert_eq!(cursor.resource, "routes.yaml");
686 assert_eq!(cursor.line, Some(42));
687 assert_eq!(cursor.column, Some(10));
688 }
689
690 #[test]
691 fn test_cursor_position_without_line_column() {
692 let cursor = CursorPosition {
693 resource: "config.json".to_string(),
694 line: None,
695 column: None,
696 };
697
698 assert!(cursor.line.is_none());
699 assert!(cursor.column.is_none());
700 }
701
702 #[test]
703 fn test_cursor_position_serialization() {
704 let cursor = CursorPosition {
705 resource: "test.yaml".to_string(),
706 line: Some(1),
707 column: Some(5),
708 };
709
710 let json = serde_json::to_string(&cursor).unwrap();
711 let deserialized: CursorPosition = serde_json::from_str(&json).unwrap();
712
713 assert_eq!(deserialized.resource, "test.yaml");
714 assert_eq!(deserialized.line, Some(1));
715 }
716
717 #[test]
720 fn test_active_session_creation() {
721 let user_id = Uuid::new_v4();
722 let workspace_id = Uuid::new_v4();
723 let session_id = Uuid::new_v4();
724 let now = Utc::now();
725
726 let session = ActiveSession {
727 user_id,
728 workspace_id,
729 session_id,
730 connected_at: now,
731 last_activity: now,
732 cursor: None,
733 };
734
735 assert_eq!(session.user_id, user_id);
736 assert_eq!(session.workspace_id, workspace_id);
737 assert!(session.cursor.is_none());
738 }
739
740 #[test]
741 fn test_active_session_with_cursor() {
742 let user_id = Uuid::new_v4();
743 let workspace_id = Uuid::new_v4();
744 let session_id = Uuid::new_v4();
745 let now = Utc::now();
746
747 let cursor = CursorPosition {
748 resource: "routes.yaml".to_string(),
749 line: Some(10),
750 column: Some(5),
751 };
752
753 let session = ActiveSession {
754 user_id,
755 workspace_id,
756 session_id,
757 connected_at: now,
758 last_activity: now,
759 cursor: Some(cursor),
760 };
761
762 assert!(session.cursor.is_some());
763 assert_eq!(session.cursor.as_ref().unwrap().resource, "routes.yaml");
764 }
765
766 #[test]
767 fn test_active_session_serialization() {
768 let session = ActiveSession {
769 user_id: Uuid::new_v4(),
770 workspace_id: Uuid::new_v4(),
771 session_id: Uuid::new_v4(),
772 connected_at: Utc::now(),
773 last_activity: Utc::now(),
774 cursor: None,
775 };
776
777 let json = serde_json::to_string(&session).unwrap();
778 let deserialized: ActiveSession = serde_json::from_str(&json).unwrap();
779
780 assert_eq!(deserialized.user_id, session.user_id);
781 }
782}