use crate::application::dto::DiffRequest;
use crate::ports::outbound::{DiffLockfileReader, DiffSource, LockfileReader};
use crate::sbom_generation::domain::dependency_diff::DependencyDiff;
use crate::sbom_generation::domain::services::DependencyDiffAnalyzer;
use crate::shared::Result;
pub struct GenerateDiffUseCase<LR, DLR>
where
LR: LockfileReader,
DLR: DiffLockfileReader,
{
lockfile_reader: LR,
diff_reader: DLR,
}
impl<LR, DLR> GenerateDiffUseCase<LR, DLR>
where
LR: LockfileReader,
DLR: DiffLockfileReader,
{
pub fn new(lockfile_reader: LR, diff_reader: DLR) -> Self {
Self {
lockfile_reader,
diff_reader,
}
}
pub fn execute(&self, request: DiffRequest) -> Result<DependencyDiff> {
let (current, _deps) = self
.lockfile_reader
.read_and_parse_lockfile(&request.project_path)?;
let base = self
.diff_reader
.read_base_packages(&request.source, &request.project_path)?;
let mut diff = DependencyDiffAnalyzer::analyze(&base, ¤t);
diff.base_ref = match &request.source {
DiffSource::GitRef(r) => r.clone(),
DiffSource::FilePath(p) => p.to_string_lossy().into_owned(),
};
let _ = request.check_cve;
Ok(diff)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ports::outbound::lockfile_reader::LockfileParseResult;
use crate::sbom_generation::domain::dependency_diff::ChangeType;
use crate::sbom_generation::domain::Package;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
fn pkg(name: &str, version: &str) -> Package {
Package::new(name.to_string(), version.to_string()).unwrap()
}
struct StubLockfileReader {
packages: Vec<Package>,
}
impl LockfileReader for StubLockfileReader {
fn read_lockfile(&self, _project_path: &Path) -> Result<String> {
Ok(String::new())
}
fn read_and_parse_lockfile(&self, _project_path: &Path) -> Result<LockfileParseResult> {
Ok((self.packages.clone(), HashMap::new()))
}
fn read_and_parse_lockfile_for_member(
&self,
_project_path: &Path,
_member_name: &str,
) -> Result<LockfileParseResult> {
Ok((self.packages.clone(), HashMap::new()))
}
}
struct StubDiffLockfileReader {
packages: Vec<Package>,
}
impl DiffLockfileReader for StubDiffLockfileReader {
fn read_base_packages(
&self,
_source: &DiffSource,
_project_path: &Path,
) -> Result<Vec<Package>> {
Ok(self.packages.clone())
}
}
fn make_use_case(
current: Vec<Package>,
base: Vec<Package>,
) -> GenerateDiffUseCase<StubLockfileReader, StubDiffLockfileReader> {
GenerateDiffUseCase::new(
StubLockfileReader { packages: current },
StubDiffLockfileReader { packages: base },
)
}
fn git_ref_request(ref_name: &str) -> DiffRequest {
DiffRequest {
source: DiffSource::GitRef(ref_name.to_string()),
project_path: PathBuf::from("/project"),
check_cve: false,
}
}
#[test]
fn test_execute_detects_added_package() {
let uc = make_use_case(vec![pkg("requests", "2.31.0")], vec![]);
let diff = uc.execute(git_ref_request("main")).unwrap();
assert_eq!(diff.changes.len(), 1);
assert_eq!(diff.changes[0].change_type, ChangeType::Added);
assert_eq!(diff.changes[0].package_name, "requests");
assert_eq!(diff.summary.added, 1);
}
#[test]
fn test_execute_detects_removed_package() {
let uc = make_use_case(vec![], vec![pkg("requests", "2.31.0")]);
let diff = uc.execute(git_ref_request("main")).unwrap();
assert_eq!(diff.changes.len(), 1);
assert_eq!(diff.changes[0].change_type, ChangeType::Removed);
assert_eq!(diff.summary.removed, 1);
}
#[test]
fn test_execute_detects_updated_package() {
let uc = make_use_case(
vec![pkg("urllib3", "2.0.7")],
vec![pkg("urllib3", "1.26.5")],
);
let diff = uc.execute(git_ref_request("main")).unwrap();
assert_eq!(diff.changes.len(), 1);
assert_eq!(diff.changes[0].change_type, ChangeType::Updated);
assert_eq!(diff.changes[0].old_version, Some("1.26.5".to_string()));
assert_eq!(diff.changes[0].new_version, Some("2.0.7".to_string()));
assert_eq!(diff.summary.updated, 1);
}
#[test]
fn test_execute_mixed_changes() {
let base = vec![pkg("a", "1.0"), pkg("b", "1.0"), pkg("c", "1.0")];
let current = vec![pkg("a", "1.0"), pkg("b", "2.0"), pkg("d", "1.0")];
let uc = make_use_case(current, base);
let diff = uc.execute(git_ref_request("main")).unwrap();
assert_eq!(diff.summary.added, 1);
assert_eq!(diff.summary.updated, 1);
assert_eq!(diff.summary.removed, 1);
assert_eq!(diff.summary.unchanged, 1);
}
#[test]
fn test_execute_base_ref_uses_git_ref_string() {
let uc = make_use_case(vec![], vec![]);
let diff = uc.execute(git_ref_request("main")).unwrap();
assert_eq!(diff.base_ref, "main");
}
#[test]
fn test_execute_base_ref_uses_file_path_string() {
let uc = make_use_case(vec![], vec![]);
let req = DiffRequest {
source: DiffSource::FilePath(PathBuf::from("/tmp/uv.lock")),
project_path: PathBuf::from("/project"),
check_cve: false,
};
let diff = uc.execute(req).unwrap();
assert_eq!(diff.base_ref, "/tmp/uv.lock");
}
#[test]
fn test_execute_check_cve_flag_is_currently_a_noop() {
let base = vec![pkg("a", "1.0")];
let current = vec![pkg("a", "2.0")];
let uc_false = make_use_case(current.clone(), base.clone());
let uc_true = make_use_case(current, base);
let diff_false = uc_false.execute(git_ref_request("main")).unwrap();
let mut req_cve = git_ref_request("main");
req_cve.check_cve = true;
let diff_true = uc_true.execute(req_cve).unwrap();
assert_eq!(diff_false.changes, diff_true.changes);
assert_eq!(diff_false.summary, diff_true.summary);
}
}