From 3f6745be45808f93cf8c8a4b65431556bbb9de8e Mon Sep 17 00:00:00 2001 From: thibaud-leclere Date: Mon, 13 Apr 2026 09:54:52 +0200 Subject: [PATCH] feat: Project model with CRUD operations and tests Co-Authored-By: Claude Sonnet 4.6 --- src-tauri/src/lib.rs | 1 + src-tauri/src/models/mod.rs | 1 + src-tauri/src/models/project.rs | 184 ++++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 src-tauri/src/models/mod.rs create mode 100644 src-tauri/src/models/project.rs diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index db8ceca..7d4545b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ mod db; mod error; +mod models; use std::sync::Mutex; use tauri::Manager; diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs new file mode 100644 index 0000000..36df406 --- /dev/null +++ b/src-tauri/src/models/mod.rs @@ -0,0 +1 @@ +pub mod project; diff --git a/src-tauri/src/models/project.rs b/src-tauri/src/models/project.rs new file mode 100644 index 0000000..66f4f2b --- /dev/null +++ b/src-tauri/src/models/project.rs @@ -0,0 +1,184 @@ +use rusqlite::{params, Connection, Result}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, + pub name: String, + pub path: String, + pub cloned_from: Option, + pub base_branch: String, + pub created_at: String, +} + +impl Project { + pub fn insert( + conn: &Connection, + name: &str, + path: &str, + cloned_from: Option<&str>, + base_branch: &str, + ) -> Result { + let id = Uuid::new_v4().to_string(); + let now = chrono::Utc::now().to_rfc3339(); + + conn.execute( + "INSERT INTO projects (id, name, path, cloned_from, base_branch, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![id, name, path, cloned_from, base_branch, now], + )?; + + Ok(Project { + id, + name: name.to_string(), + path: path.to_string(), + cloned_from: cloned_from.map(String::from), + base_branch: base_branch.to_string(), + created_at: now, + }) + } + + pub fn list(conn: &Connection) -> Result> { + let mut stmt = conn.prepare( + "SELECT id, name, path, cloned_from, base_branch, created_at FROM projects ORDER BY created_at DESC", + )?; + let rows = stmt.query_map([], |row| { + Ok(Project { + id: row.get(0)?, + name: row.get(1)?, + path: row.get(2)?, + cloned_from: row.get(3)?, + base_branch: row.get(4)?, + created_at: row.get(5)?, + }) + })?; + rows.collect() + } + + pub fn get_by_id(conn: &Connection, id: &str) -> Result { + conn.query_row( + "SELECT id, name, path, cloned_from, base_branch, created_at FROM projects WHERE id = ?1", + params![id], + |row| { + Ok(Project { + id: row.get(0)?, + name: row.get(1)?, + path: row.get(2)?, + cloned_from: row.get(3)?, + base_branch: row.get(4)?, + created_at: row.get(5)?, + }) + }, + ) + } + + pub fn update(conn: &Connection, id: &str, name: &str, base_branch: &str) -> Result<()> { + conn.execute( + "UPDATE projects SET name = ?1, base_branch = ?2 WHERE id = ?3", + params![name, base_branch, id], + )?; + Ok(()) + } + + pub fn delete(conn: &Connection, id: &str) -> Result<()> { + conn.execute("DELETE FROM projects WHERE id = ?1", params![id])?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db; + + fn setup() -> Connection { + db::init_in_memory().expect("db init should succeed") + } + + #[test] + fn test_insert_project_local_path() { + let conn = setup(); + let project = Project::insert(&conn, "My Project", "/home/user/code/myproject", None, "main") + .expect("insert should succeed"); + + assert_eq!(project.name, "My Project"); + assert_eq!(project.path, "/home/user/code/myproject"); + assert!(project.cloned_from.is_none()); + assert_eq!(project.base_branch, "main"); + assert!(!project.id.is_empty()); + assert!(!project.created_at.is_empty()); + } + + #[test] + fn test_insert_project_cloned() { + let conn = setup(); + let project = Project::insert( + &conn, + "Cloned Project", + "/home/user/code/cloned", + Some("https://github.com/org/repo.git"), + "stable", + ) + .expect("insert should succeed"); + + assert_eq!(project.cloned_from.as_deref(), Some("https://github.com/org/repo.git")); + assert_eq!(project.base_branch, "stable"); + } + + #[test] + fn test_list_projects_empty() { + let conn = setup(); + let projects = Project::list(&conn).expect("list should succeed"); + assert!(projects.is_empty()); + } + + #[test] + fn test_list_projects_returns_all() { + let conn = setup(); + Project::insert(&conn, "A", "/path/a", None, "main").unwrap(); + Project::insert(&conn, "B", "/path/b", None, "main").unwrap(); + + let projects = Project::list(&conn).expect("list should succeed"); + assert_eq!(projects.len(), 2); + } + + #[test] + fn test_get_by_id() { + let conn = setup(); + let created = Project::insert(&conn, "Test", "/path/test", None, "main").unwrap(); + let found = Project::get_by_id(&conn, &created.id).expect("get should succeed"); + + assert_eq!(found.id, created.id); + assert_eq!(found.name, "Test"); + } + + #[test] + fn test_get_by_id_not_found() { + let conn = setup(); + let result = Project::get_by_id(&conn, "nonexistent"); + assert!(result.is_err()); + } + + #[test] + fn test_update_project() { + let conn = setup(); + let created = Project::insert(&conn, "Old Name", "/path", None, "main").unwrap(); + + Project::update(&conn, &created.id, "New Name", "develop").expect("update should succeed"); + + let updated = Project::get_by_id(&conn, &created.id).unwrap(); + assert_eq!(updated.name, "New Name"); + assert_eq!(updated.base_branch, "develop"); + } + + #[test] + fn test_delete_project() { + let conn = setup(); + let created = Project::insert(&conn, "ToDelete", "/path", None, "main").unwrap(); + + Project::delete(&conn, &created.id).expect("delete should succeed"); + + let result = Project::get_by_id(&conn, &created.id); + assert!(result.is_err()); + } +}