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<()> { let affected = conn.execute( "UPDATE projects SET name = ?1, base_branch = ?2 WHERE id = ?3", params![name, base_branch, id], )?; if affected == 0 { return Err(rusqlite::Error::QueryReturnedNoRows); } Ok(()) } pub fn delete(conn: &Connection, id: &str) -> Result<()> { let affected = conn.execute("DELETE FROM projects WHERE id = ?1", params![id])?; if affected == 0 { return Err(rusqlite::Error::QueryReturnedNoRows); } 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()); } }