feat: Project model with CRUD operations and tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thibaud-leclere 2026-04-13 09:54:52 +02:00
parent afd092c642
commit 3f6745be45
3 changed files with 186 additions and 0 deletions

View file

@ -1,5 +1,6 @@
mod db;
mod error;
mod models;
use std::sync::Mutex;
use tauri::Manager;

View file

@ -0,0 +1 @@
pub mod project;

View file

@ -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<String>,
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<Project> {
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<Vec<Project>> {
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<Project> {
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());
}
}