From 60be856fdce5a7f396120261e443d4d8141af91a Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Tue, 11 Nov 2025 12:37:13 +0100 Subject: [PATCH 1/7] refactor: pull out common code Assisted-By: Cursor --- .../src/endpoints/tests/latest_filters.rs | 356 +++++++++--------- modules/analysis/src/endpoints/tests/mod.rs | 1 + modules/analysis/src/endpoints/tests/req.rs | 74 ++++ 3 files changed, 250 insertions(+), 181 deletions(-) create mode 100644 modules/analysis/src/endpoints/tests/req.rs diff --git a/modules/analysis/src/endpoints/tests/latest_filters.rs b/modules/analysis/src/endpoints/tests/latest_filters.rs index 03c05e030..517ea8505 100644 --- a/modules/analysis/src/endpoints/tests/latest_filters.rs +++ b/modules/analysis/src/endpoints/tests/latest_filters.rs @@ -1,11 +1,9 @@ +use super::req::*; use crate::test::caller; - -use actix_http::Request; -use actix_web::test::TestRequest; -use serde_json::{Value, json}; +use serde_json::json; use test_context::test_context; use test_log::test; -use trustify_test_context::{TrustifyContext, call::CallService, subset::ContainsSubset}; +use trustify_test_context::{TrustifyContext, subset::ContainsSubset}; #[test_context(TrustifyContext)] #[test(actix_web::test)] @@ -24,65 +22,59 @@ async fn resolve_rh_variant_latest_filter_container_cdx( ]) .await?; - let uri: String = "/api/v2/analysis/component".to_string(); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response = app.call_service(request).await; - assert_eq!(200, response.response().status()); + let _response = app.req(Req::default()).await?; // cpe search - let uri: String = format!( - "/api/v2/analysis/component/{}", - urlencoding::encode("cpe:/a:redhat:quay:3::el8") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; - assert!(response.contains_subset(json!({ - "total":2 - }))); + let response = app + .req(Req { + loc: Loc::Id("cpe:/a:redhat:quay:3::el8"), + ..Req::default() + }) + .await?; + assert_eq!(2, response["total"]); // cpe latest search - let uri: String = format!( - "/api/v2/analysis/latest/component/{}", - urlencoding::encode("cpe:/a:redhat:quay:3::el8") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; - assert!(response.contains_subset(json!({ - "total":1 - }))); + let response = app + .req(Req { + latest: true, + loc: Loc::Id("cpe:/a:redhat:quay:3::el8"), + ..Req::default() + }) + .await?; + assert_eq!(1, response["total"]); // purl partial search - let uri: String = format!( - "/api/v2/analysis/component?q={}&ancestors=10", - urlencoding::encode("pkg:oci/quay-builder-qemu-rhcos-rhel8") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + loc: Loc::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), + ancestors: Some(10), + ..Req::default() + }) + .await?; assert_eq!(18, response["total"]); // purl partial search latest - let uri: String = format!( - "/api/v2/analysis/latest/component?q={}&ancestors=10", - urlencoding::encode("pkg:oci/quay-builder-qemu-rhcos-rhel8") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; - log::warn!("{:?}", response.get("total")); - assert!(response.contains_subset(json!({ - "total":2 - }))); + let response = app + .req(Req { + latest: true, + loc: Loc::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), + ancestors: Some(10), + ..Req::default() + }) + .await?; + assert_eq!(2, response["total"]); // purl partial search latest - let uri: String = format!( - "/api/v2/analysis/latest/component?q={}&ancestors=10", - urlencoding::encode("purl:name~quay-builder-qemu-rhcos-rhel8&purl:ty=oci") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; - log::warn!("{:?}", response.get("total")); - assert!(response.contains_subset(json!({ - "total":7 - }))); + let response = app + .req(Req { + latest: true, + loc: Loc::Q("purl:name~quay-builder-qemu-rhcos-rhel8&purl:ty=oci"), + ancestors: Some(10), + ..Req::default() + }) + .await?; + assert_eq!(7, response["total"]); + Ok(()) } @@ -101,72 +93,74 @@ async fn resolve_rh_variant_latest_filter_rpms_cdx( ]) .await?; - let uri: String = "/api/v2/analysis/component".to_string(); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response = app.call_service(request).await; - assert_eq!(200, response.response().status()); + let _response = app.req(Req::default()).await?; // cpe search - let uri: String = format!( - "/api/v2/analysis/component/{}", - urlencoding::encode("cpe:/a:redhat:rhel_eus:9.4::crb") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 2); // cpe latest search - let uri: String = format!( - "/api/v2/analysis/latest/component/{}", - urlencoding::encode("cpe:/a:redhat:rhel_eus:9.4::crb") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + latest: true, + loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 1); // purl partial search - let uri: String = format!( - "/api/v2/analysis/component?q={}&ancestors=10", - urlencoding::encode("pkg:rpm/redhat/NetworkManager-libnm") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm"), + ancestors: Some(10), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 30); // purl partial latest search - let uri: String = format!( - "/api/v2/analysis/latest/component?q={}", - urlencoding::encode("pkg:rpm/redhat/NetworkManager-libnm") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + latest: true, + loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 15); // purl more specific latest q search - let uri: String = format!( - "/api/v2/analysis/latest/component?q={}", - urlencoding::encode("pkg:rpm/redhat/NetworkManager-libnm-devel@") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + latest: true, + loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm-devel@"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 5); // name exact search - let uri: String = format!( - "/api/v2/analysis/component/{}", - urlencoding::encode("NetworkManager-libnm-devel") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + loc: Loc::Id("NetworkManager-libnm-devel"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 10); // latest name exact search - let uri: String = format!( - "/api/v2/analysis/latest/component/{}", - urlencoding::encode("NetworkManager-libnm-devel") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + latest: true, + loc: Loc::Id("NetworkManager-libnm-devel"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 5); Ok(()) @@ -189,63 +183,64 @@ async fn resolve_rh_variant_latest_filter_middleware_cdx( ]) .await?; - let uri: String = "/api/v2/analysis/component".to_string(); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response = app.call_service(request).await; - assert_eq!(200, response.response().status()); + let _response = app.req(Req::default()).await?; // cpe search - let uri: String = format!( - "/api/v2/analysis/component/{}", - urlencoding::encode("cpe:/a:redhat:camel_quarkus:3") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + loc: Loc::Id("cpe:/a:redhat:camel_quarkus:3"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 2); // cpe latest search - let uri: String = format!( - "/api/v2/analysis/latest/component/{}", - urlencoding::encode("cpe:/a:redhat:camel_quarkus:3") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + latest: true, + loc: Loc::Id("cpe:/a:redhat:camel_quarkus:3"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 1); // purl partial search - let uri: String = format!( - "/api/v2/analysis/component?q={}&ancestors=10", - urlencoding::encode("pkg:maven/io.vertx/vertx-core@") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + loc: Loc::Q("pkg:maven/io.vertx/vertx-core@"), + ancestors: Some(10), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 6); // purl partial latest search - let uri: String = format!( - "/api/v2/analysis/latest/component?q={}", - urlencoding::encode("pkg:maven/io.vertx/vertx-core@") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + latest: true, + loc: Loc::Q("pkg:maven/io.vertx/vertx-core@"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 2); // name exact search - let uri: String = format!( - "/api/v2/analysis/component/{}", - urlencoding::encode("vertx-core") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + loc: Loc::Id("vertx-core"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 6); // latest name exact search - let uri: String = format!( - "/api/v2/analysis/latest/component/{}", - urlencoding::encode("vertx-core") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + latest: true, + loc: Loc::Id("vertx-core"), + ..Req::default() + }) + .await?; assert_eq!(response["total"], 2); Ok(()) @@ -266,18 +261,17 @@ async fn test_tc2606(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ]) .await?; - let uri: String = "/api/v2/analysis/component".to_string(); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response = app.call_service(request).await; - assert_eq!(200, response.response().status()); + let _response = app.req(Req::default()).await?; // latest cpe search - let uri: String = format!( - "/api/v2/analysis/latest/component/{}?descendants=1", - urlencoding::encode("cpe:/a:redhat:rhel_eus:9.4::appstream") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + latest: true, + loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::appstream"), + descendants: Some(1), + ..Req::default() + }) + .await?; log::info!("{response:#?}"); assert_eq!(response["total"], 2); @@ -360,18 +354,18 @@ async fn test_tc2677(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ]) .await?; - let uri: String = "/api/v2/analysis/component".to_string(); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response = app.call_service(request).await; - assert_eq!(200, response.response().status()); + let _response = app.req(Req::default()).await?; // latest cpe search - let uri: String = format!( - "/api/v2/analysis/latest/component/{}?descendants=10", - urlencoding::encode("cpe:/a:redhat:3scale:2.15::el9") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let response = app + .req(Req { + latest: true, + loc: Loc::Id("cpe:/a:redhat:3scale:2.15::el9"), + descendants: Some(10), + ..Req::default() + }) + .await?; + log::info!("{response:#?}"); assert_eq!(response["total"], 1); @@ -438,17 +432,16 @@ async fn test_tc2717(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ]) .await?; - let uri: String = "/api/v2/analysis/component".to_string(); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response = app.call_service(request).await; - assert_eq!(200, response.response().status()); - - let uri: String = format!( - "/api/v2/analysis/latest/component/{}", - urlencoding::encode("pkg:maven/io.vertx/vertx-core") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let _response = app.req(Req::default()).await?; + + let response = app + .req(Req { + latest: true, + loc: Loc::Id("pkg:maven/io.vertx/vertx-core"), + ..Req::default() + }) + .await?; + assert_eq!(response["total"], 2); Ok(()) @@ -472,17 +465,17 @@ async fn test_tc2578(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ]) .await?; - let uri: String = "/api/v2/analysis/component".to_string(); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response = app.call_service(request).await; - assert_eq!(200, response.response().status()); - - let uri: String = format!( - "/api/v2/analysis/latest/component/{}?descendants=100", - urlencoding::encode("cpe:/a:redhat:jboss_enterprise_application_platform:7.4") - ); - let request: Request = TestRequest::get().uri(&uri).to_request(); - let response: Value = app.call_and_read_body_json(request).await; + let _response = app.req(Req::default()).await?; + + let response = app + .req(Req { + latest: true, + loc: Loc::Id("cpe:/a:redhat:jboss_enterprise_application_platform:7.4"), + descendants: Some(100), + ..Req::default() + }) + .await?; + assert_eq!(response["total"], 1); assert!(response.contains_subset(json!( { @@ -519,5 +512,6 @@ async fn test_tc2578(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { }], "total": 1 }))); + Ok(()) } diff --git a/modules/analysis/src/endpoints/tests/mod.rs b/modules/analysis/src/endpoints/tests/mod.rs index 721c2e501..d98475efe 100644 --- a/modules/analysis/src/endpoints/tests/mod.rs +++ b/modules/analysis/src/endpoints/tests/mod.rs @@ -1,6 +1,7 @@ mod cyclonedx; mod dot; mod latest_filters; +pub mod req; mod rh_variant; mod spdx; diff --git a/modules/analysis/src/endpoints/tests/req.rs b/modules/analysis/src/endpoints/tests/req.rs new file mode 100644 index 000000000..205befc29 --- /dev/null +++ b/modules/analysis/src/endpoints/tests/req.rs @@ -0,0 +1,74 @@ +use crate::endpoints::tests::CallService; +use actix_http::{Request, StatusCode}; +use actix_web::test::TestRequest; +use serde_json::Value; + +#[derive(Default, Copy, Clone)] +pub struct Req<'a> { + pub latest: bool, + pub ancestors: Option, + pub descendants: Option, + pub loc: Loc<'a>, +} + +#[derive(Default, Copy, Clone)] +pub enum Loc<'a> { + #[default] + None, + Q(&'a str), + Id(&'a str), +} + +pub trait ReqExt { + async fn req(&self, req: Req<'_>) -> anyhow::Result; +} + +impl ReqExt for C { + async fn req(&self, req: Req<'_>) -> anyhow::Result { + let Req { + latest, + loc, + ancestors, + descendants, + } = req; + + let latest = match latest { + true => "latest/", + false => "", + }; + + let mut uri = match loc { + Loc::None => { + format!("/api/v2/analysis/{latest}component?",) + } + Loc::Q(q) => { + format!( + "/api/v2/analysis/{latest}component?q={q}&", + q = urlencoding::encode(q), + ) + } + Loc::Id(id) => { + format!( + "/api/v2/analysis/{latest}component/{id}?", + id = urlencoding::encode(id), + ) + } + }; + + if let Some(ancestors) = ancestors { + uri = format!("{uri}ancestors={ancestors}&"); + } + + if let Some(descendants) = descendants { + uri = format!("{uri}descendants={descendants}&"); + } + + let request: Request = TestRequest::get().uri(&uri).to_request(); + + let response = self.call_service(request).await; + + assert_eq!(response.status(), StatusCode::OK); + + Ok(actix_web::test::read_body_json(response).await) + } +} From 905a17379cb6a49dd5b3340dd2db235a980a8ee6 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Tue, 11 Nov 2025 16:29:51 +0100 Subject: [PATCH 2/7] test: sort result to make them comparable --- modules/analysis/src/endpoints/tests/latest_filters.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/analysis/src/endpoints/tests/latest_filters.rs b/modules/analysis/src/endpoints/tests/latest_filters.rs index 517ea8505..3645ec1d9 100644 --- a/modules/analysis/src/endpoints/tests/latest_filters.rs +++ b/modules/analysis/src/endpoints/tests/latest_filters.rs @@ -1,6 +1,6 @@ use super::req::*; use crate::test::caller; -use serde_json::json; +use serde_json::{Value, json}; use test_context::test_context; use test_log::test; use trustify_test_context::{TrustifyContext, subset::ContainsSubset}; From 34c10bdb1b1089bf049a817a40169e7b19eb81ed Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Tue, 18 Nov 2025 08:18:21 +0100 Subject: [PATCH 3/7] refactor: restructure code, add tests --- modules/analysis/src/service/collector.rs | 278 ++++++++++++---------- modules/analysis/src/service/load.rs | 139 ++++++----- modules/analysis/src/service/mod.rs | 188 ++++++++------- modules/analysis/src/service/test.rs | 16 +- 4 files changed, 332 insertions(+), 289 deletions(-) diff --git a/modules/analysis/src/service/collector.rs b/modules/analysis/src/service/collector.rs index 54c47762f..2b5ee2ec0 100644 --- a/modules/analysis/src/service/collector.rs +++ b/modules/analysis/src/service/collector.rs @@ -1,4 +1,5 @@ use super::*; +use crate::model::graph::{ExternalNode, PackageNode}; use futures::stream::{self, StreamExt}; use parking_lot::Mutex; use std::{collections::HashMap, sync::Arc}; @@ -113,148 +114,157 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> { /// /// If the depth is zero, or the node was already processed, it will return [`None`], indicating /// that the request was not processed. - pub async fn collect(self) -> Option> { + pub async fn collect(self) -> Result>, Error> { tracing::debug!(direction = ?self.direction, "collecting for {:?}", self.node); if self.depth == 0 { log::debug!("depth is zero"); // we ran out of depth - return None; + return Ok(None); } if !self.discovered.visit(self.graph, self.node) { log::debug!("node got visited already"); // we've already seen this - return None; + return Ok(None); } match self.graph.node_weight(self.node) { Some(graph::Node::External(external_node)) => { - // we know this is an external node, so retrieve external sbom descendant nodes - let ResolvedSbom { - sbom_id: external_sbom_id, - node_id: external_node_id, - } = resolve_external_sbom(&external_node.node_id, self.connection).await?; - - // retrieve external sbom graph from graph_cache - let Some(external_graph) = self.graph_cache.get(&external_sbom_id.to_string()) - else { - log::warn!( - "external sbom graph {:?} for {:?} not found during collection.", - &external_sbom_id.to_string(), - &external_node_id.to_string() - ); - return None; - }; - // find the node in retrieved external graph - let Some(external_node_index) = external_graph - .node_indices() - .find(|&node| external_graph[node].node_id.eq(&external_node_id)) - else { - log::warn!("Node with ID {external_node_id} not found in external sbom"); - return None; - }; - // recurse into those descendent nodes - Some( - self.with(external_graph.as_ref(), external_node_index) - .collect_graph() - .await, - ) - } - Some(graph::Node::Package(current_node)) => { - // collect external sbom ancestor nodes - let current_sbom_id = ¤t_node.sbom_id; - let current_sbom_uuid = *current_sbom_id; - let current_node_id = ¤t_node.node_id; - - let find_sbom_externals = resolve_rh_external_sbom_ancestors( - current_sbom_uuid, - current_node.node_id.clone().to_string(), - self.connection, - ) - .await; - - let resolved_external_nodes: Vec = stream::iter(find_sbom_externals) - .map(|sbom_external_node| { - let collector = self.clone(); - async move { - if &sbom_external_node.sbom_id == current_sbom_id { - return None; - } - // check this is a valid external relationship - match sbom_external_node::Entity::find() - .filter( - sbom_external_node::Column::SbomId - .eq(sbom_external_node.clone().sbom_id), - ) - .filter( - sbom_external_node::Column::ExternalNodeRef - .eq(sbom_external_node.clone().node_id), - ) - .one(collector.connection) - .await - { - Ok(Some(matched)) => { - // get the external sbom graph - let Some(external_graph) = - collector.graph_cache.clone().get(&matched.sbom_id.to_string()) - else { - log::warn!( - "external sbom graph {:?} not found in graph cache", - &matched.sbom_id.to_string() - ); - return None; - }; - // find the node in retrieved external graph - let Some(external_node_index) = external_graph - .node_indices() - .find(|&node| { - external_graph[node].node_id.eq(&matched.node_id) - }) - else { - log::warn!( - "Node with ID {current_node_id} not found in external sbom" - ); - return None; - }; - // recurse into those external sbom nodes and save - Some( - collector - .with(external_graph.as_ref(), external_node_index) - .collect_graph() - .await, - ) - } - Err(_) => { - log::warn!("Problem looking up sbom external node"); - None - } - _ => { - log::debug!( - "not external sbom sbom_external_node {sbom_external_node:?}" - ); - None - } - } - } - }) - .buffer_unordered(self.concurrency) - .filter_map(|nodes| async move { nodes }) - .flat_map(stream::iter) - .collect::>() - .await; - - let mut result = self.collect_graph().await; - if !resolved_external_nodes.is_empty() { - result.extend(resolved_external_nodes); - } - Some(result) + self.collect_external(external_node).await } - _ => Some(self.collect_graph().await), + Some(graph::Node::Package(current_node)) => self.collect_package(current_node).await, + _ => Ok(Some(self.collect_graph().await?)), } } - pub async fn collect_graph(&self) -> Vec { + async fn collect_external( + self, + external_node: &ExternalNode, + ) -> Result>, Error> { + log::debug!( + "Collecting external node {}/{}", + external_node.sbom_id, + external_node.node_id + ); + + // we know this is an external node, so retrieve external sbom descendant nodes + let Some(ResolvedSbom { + sbom_id: external_sbom_id, + node_id: external_node_id, + }) = resolve_external_sbom(&external_node.node_id, self.connection).await? + else { + log::debug!("Unable to resolve external node: {}", external_node.node_id); + return Ok(None); + }; + + // retrieve external sbom graph from graph_cache + let Some(external_graph) = self.graph_cache.get(&external_sbom_id.to_string()) else { + log::warn!( + "external sbom graph {:?} for {:?} not found during collection.", + &external_sbom_id.to_string(), + &external_node_id.to_string() + ); + return Ok(None); + }; + + // find the node in retrieved external graph + let Some(external_node_index) = external_graph + .node_indices() + .find(|&node| external_graph[node].node_id.eq(&external_node_id)) + else { + log::warn!("Node with ID {external_node_id} not found in external sbom"); + return Ok(None); + }; + + // recurse into those descendent nodes + Ok(Some( + self.with(external_graph.as_ref(), external_node_index) + .collect_graph() + .await?, + )) + } + + async fn collect_package(self, current_node: &PackageNode) -> Result>, Error> { + // collect external sbom ancestor nodes + let current_sbom_id = ¤t_node.sbom_id; + let current_sbom_uuid = *current_sbom_id; + let current_node_id = ¤t_node.node_id; + + let find_sbom_externals = resolve_rh_external_sbom_ancestors( + current_sbom_uuid, + current_node.node_id.clone().to_string(), + self.connection, + ) + .await?; + + let resolved_external_nodes: Vec = stream::iter(find_sbom_externals) + .map(|sbom_external_node| { + let collector = self.clone(); + async move { + if &sbom_external_node.sbom_id == current_sbom_id { + return Ok::<_, Error>(vec![]); + } + + // check this is a valid external relationship + + let Some(matched) = sbom_external_node::Entity::find() + .filter(sbom_external_node::Column::SbomId.eq(sbom_external_node.sbom_id)) + .filter( + sbom_external_node::Column::ExternalNodeRef + .eq(&sbom_external_node.node_id), + ) + .one(collector.connection) + .await? + else { + log::debug!("no external sbom sbom_external_node {sbom_external_node:?}"); + return Ok(vec![]); + }; + + // get the external sbom graph + + let Some(external_graph) = collector + .graph_cache + .clone() + .get(&matched.sbom_id.to_string()) + else { + log::warn!( + "external sbom graph {} not found in graph cache", + matched.sbom_id + ); + return Ok(vec![]); + }; + + // find the node in retrieved external graph + + let Some(external_node_index) = external_graph + .node_indices() + .find(|&node| external_graph[node].node_id.eq(&matched.node_id)) + else { + log::warn!("Node with ID {current_node_id} not found in external sbom"); + return Ok(vec![]); + }; + + // recurse into those external sbom nodes and save + + Ok(collector + .with(external_graph.as_ref(), external_node_index) + .collect_graph() + .await?) + } + }) + .buffer_unordered(self.concurrency) + .map_ok(|nodes| stream::iter(nodes.into_iter().map(Ok::<_, Error>))) + .try_flatten() + .try_collect() + .await?; + + let mut result = self.collect_graph().await?; + result.extend(resolved_external_nodes); + Ok(Some(result)) + } + + pub async fn collect_graph(&self) -> Result, Error> { log::debug!("Collecting graph for {:?}", self.node); stream::iter(self.graph.edges_directed(self.node, self.direction)) @@ -267,7 +277,7 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> { // If the direction is incoming, we are collecting ancestors. // We recursively call `collect` for the source of the edge. Direction::Incoming => ( - self.continue_node(edge.source()).collect().await, + self.continue_node(edge.source()).collect().await?, None, self.graph.node_weight(edge.source()), ), @@ -275,7 +285,7 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> { // We recursively call `collect` for the target of the edge. Direction::Outgoing => ( None, - self.continue_node(edge.target()).collect().await, + self.continue_node(edge.target()).collect().await?, self.graph.node_weight(edge.target()), ), }; @@ -284,20 +294,24 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> { if !self.relationships.is_empty() && !self.relationships.contains(relationship) { // if we have entries, and no match, continue with the next - return None; + return Ok(None); } + let Some(package_node) = package_node else { + return Ok(None); + }; + // Create a new `Node` and add it to the result - Some(Node { - base: BaseSummary::from(package_node?), + Ok(Some(Node { + base: BaseSummary::from(package_node), relationship: Some(*relationship), ancestors: ancestor, descendants: descendent, - }) + })) }) .buffer_unordered(self.concurrency) - .filter_map(|node| async move { node }) - .collect::>() + .try_filter_map(|x| async move { Ok(x) }) // drop None + .try_collect::>() .await } } diff --git a/modules/analysis/src/service/load.rs b/modules/analysis/src/service/load.rs index faf817c84..a8c098d0f 100644 --- a/modules/analysis/src/service/load.rs +++ b/modules/analysis/src/service/load.rs @@ -33,6 +33,7 @@ use tracing::{Level, instrument}; use trustify_common::{ cpe::Cpe as TrustifyCpe, db::query::{Columns, Filtering, IntoColumns}, + id::IdError, purl::Purl, }; use trustify_entity::{ @@ -381,13 +382,13 @@ impl InnerService { /// which returns correctly because name related filters are already performed. /// /// It is worth mentioning that aforementioned CPE queries cannot use CPE only partitioning because we - /// need to discriminate on component name (across CPEs) hence special casing both code paths. + /// need to discriminate on component name (across CPEs) hence special casing both code paths. /// /// /// # Returns /// /// An unexecuted `sea_orm::Select` query builder struct. - /// + /// fn build_ranked_query( sbom_package_relation: sbom_node::Relation, partition_by_name: bool, @@ -737,82 +738,92 @@ impl InnerService { distinct_sbom_ids.len() ); - let mut seen_sbom_ids: HashSet = HashSet::new(); - self.load_graphs_inner(connection, distinct_sbom_ids, &mut seen_sbom_ids) + self.load_graphs_inner(connection, distinct_sbom_ids, &mut HashSet::new()) .await } + /// Load a list of graphs, and also load related graphs. + #[instrument( + skip_all, + err(level=tracing::Level::INFO) + )] pub async fn load_graphs_inner( &self, connection: &C, + // TODO: distinct_sbom_ids: impl IntoIterator + Debug>, distinct_sbom_ids: &[impl AsRef + Debug], seen_sbom_ids: &mut HashSet, ) -> Result)>, Error> { let mut results = Vec::new(); - for distinct_sbom_id in distinct_sbom_ids.iter().map(AsRef::as_ref) { - log::debug!("loading sbom: {:?}", distinct_sbom_id); - let current_sbom_id_str = distinct_sbom_id.to_string(); - if seen_sbom_ids.insert(current_sbom_id_str.clone()) { - // at this stage we just have sbom_id string, so have to convert back to uuid - let distinct_sbom_id_uuid = match Uuid::parse_str(distinct_sbom_id) { - Ok(uuid) => uuid, - Err(e) => { - log::error!( - "Failed to parse distinct_sbom_id '{distinct_sbom_id:?}' as UUID: {e:?}" - ); - Uuid::nil() - } + for distinct_sbom_id in distinct_sbom_ids { + let distinct_sbom_id = distinct_sbom_id.as_ref(); + log::debug!("loading sbom: {distinct_sbom_id}"); + + // if we can insert into the seen map, we need to process... + if !seen_sbom_ids.insert(distinct_sbom_id.to_string()) { + // ... otherwise, we already did and can move on. + log::debug!("Skipping duplicate SBOM ID: {distinct_sbom_id}"); + continue; + } + + // load and remember the result + results.push(( + distinct_sbom_id.to_string(), + self.load_graph(connection, distinct_sbom_id).await?, + )); + + // at this stage we just have sbom_id string, so have to convert back to uuid + let distinct_sbom_id_uuid = + Uuid::parse_str(distinct_sbom_id).map_err(IdError::InvalidUuid)?; + + // select all related external nodes + let external_sboms = sbom_external_node::Entity::find() + .filter(sbom_external_node::Column::SbomId.eq(distinct_sbom_id_uuid)) + .all(connection) + .await?; + + log::debug!( + "Found {} external nodes (for SBOM: {distinct_sbom_id_uuid})", + external_sboms.len() + ); + + let mut resolved = Vec::new(); + + // resolve and load externally referenced sboms + for external_sbom in &external_sboms { + log::debug!( + "resolving external: {}/{}", + external_sbom.sbom_id, + external_sbom.node_id + ); + let resolved_external_sbom = + resolve_external_sbom(&external_sbom.node_id, connection).await?; + + let Some(resolved_external_sbom) = resolved_external_sbom else { + log::warn!("Cannot find external sbom {}", external_sbom.node_id); + continue; }; - // select all related external nodes - let external_sboms = sbom_external_node::Entity::find() - .filter(sbom_external_node::Column::SbomId.eq(distinct_sbom_id_uuid)) - .all(connection) - .await?; - - //resolve and load externally referenced sboms - for external_sbom in &external_sboms { - let resolved_external_sbom = - resolve_external_sbom(&external_sbom.node_id, connection).await; - log::debug!("resolved external sbom: {:?}", resolved_external_sbom); - if let Some(resolved_external_sbom) = resolved_external_sbom { - let resolved_external_sbom_id_str = - resolved_external_sbom.sbom_id.to_string(); - if seen_sbom_ids.insert(resolved_external_sbom_id_str.clone()) { - results.push(( - resolved_external_sbom_id_str, - self.load_graph( - connection, - &resolved_external_sbom.sbom_id.to_string(), - ) - .await?, - )); - // recurse into to find nested external sboms - Box::pin(self.load_graphs_inner( - connection, - &[resolved_external_sbom.sbom_id.to_string()], - seen_sbom_ids, - )) - .await?; - } else { - log::debug!( - "Skipping duplicate external SBOM ID: {}", - resolved_external_sbom_id_str - ); - } - } else { - log::debug!("Cannot find external sbom {:?}", external_sbom.node_id); - continue; - } - } - results.push(( - current_sbom_id_str, - self.load_graph(connection, distinct_sbom_id).await?, - )); - } else { - log::debug!("Skipping duplicate SBOM ID: {}", current_sbom_id_str); + log::debug!("resolved external sbom: {:?}", resolved_external_sbom); + + resolved.push(resolved_external_sbom.sbom_id.to_string()); + } + + log::debug!("Resolved {} SBOMs", resolved.len()); + if log::log_enabled!(log::Level::Debug) { + for r in &resolved { + log::debug!(" Resolved: {r}"); + } } + + let sub = Box::pin(async { + self.load_graphs_inner(connection, &resolved, seen_sbom_ids) + .await + }) + .await?; + + results.extend(sub); } Ok(results) diff --git a/modules/analysis/src/service/mod.rs b/modules/analysis/src/service/mod.rs index 7643e328c..c3f475687 100644 --- a/modules/analysis/src/service/mod.rs +++ b/modules/analysis/src/service/mod.rs @@ -17,7 +17,7 @@ use crate::{ model::{AnalysisStatus, BaseSummary, GraphMap, Node, PackageGraph, graph}, }; use fixedbitset::FixedBitSet; -use futures::{StreamExt, stream}; +use futures::{StreamExt, TryStreamExt, stream}; use crate::model::AnalysisStatusDetails; use futures::future::Shared; @@ -99,74 +99,84 @@ struct ResolvedSbom { pub node_id: String, } +#[instrument(skip(connection), err(level=tracing::Level::INFO))] async fn resolve_external_sbom( node_id: &str, connection: &C, -) -> Option { +) -> Result, Error> { // we first lookup in sbom_external_node - let sbom_external_node = match sbom_external_node::Entity::find() + let Some(sbom_external_node) = sbom_external_node::Entity::find() .filter(sbom_external_node::Column::NodeId.eq(node_id)) .one(connection) - .await - { - Ok(Some(entity)) => entity, - _ => return None, + .await? + else { + log::debug!("External node not found: {node_id}"); + return Ok(None); }; + log::debug!("External type: {:?}", sbom_external_node.external_type); + match sbom_external_node.external_type { ExternalType::SPDX => { // For spdx, sbom_external_node discriminator_type and discriminator_value are used // to lookup sbom_id via join to SourceDocument. The node_id is just the external_node_ref. - let discriminator_value = sbom_external_node.discriminator_value?; + let Some(discriminator_type) = sbom_external_node.discriminator_type else { + return Ok(None); + }; + + let Some(discriminator_value) = sbom_external_node.discriminator_value else { + return Ok(None); + }; if discriminator_value.is_empty() { - return None; + return Ok(None); } - let query = + let mut query = sbom::Entity::find().join(JoinType::Join, sbom::Relation::SourceDocument.def()); - let query = match sbom_external_node.discriminator_type? { + match discriminator_type { DiscriminatorType::Sha256 => { - query.filter(source_document::Column::Sha256.eq(&discriminator_value)) + query = query.filter(source_document::Column::Sha256.eq(&discriminator_value)) + } + _ => { + return Ok(None); } - _ => return None, }; - match query.one(connection).await { - Ok(Some(entity)) => Some(ResolvedSbom { + Ok(match query.one(connection).await? { + Some(entity) => Some(ResolvedSbom { sbom_id: entity.sbom_id, node_id: sbom_external_node.external_node_ref, }), _ => None, - } + }) } ExternalType::CycloneDx => { // For cyclonedx, sbom_external_node discriminator_type and discriminator_value are used // we construct external_doc_id to lookup sbom_id directly from sbom entity. The node_id // is the external_node_ref - let discriminator_value = sbom_external_node.discriminator_value?; + let Some(discriminator_value) = sbom_external_node.discriminator_value else { + return Ok(None); + }; if discriminator_value.is_empty() { - return None; + return Ok(None); } let external_doc_ref = sbom_external_node.external_doc_ref; let external_doc_id = format!("urn:cdx:{external_doc_ref}/{discriminator_value}"); - match sbom::Entity::find() + Ok(sbom::Entity::find() .filter(sbom::Column::DocumentId.eq(external_doc_id)) .one(connection) - .await - { - Ok(Some(entity)) => Some(ResolvedSbom { + .await? + .map(|entity| ResolvedSbom { sbom_id: entity.sbom_id, node_id: sbom_external_node.external_node_ref, - }), - _ => None, - } + })) } ExternalType::RedHatProductComponent => { // for RH variations we assume the sbom_external_node_ref is the package checksum @@ -183,78 +193,86 @@ async fn resolve_external_sbom( } } +#[instrument(skip(connection), err(level=tracing::Level::INFO))] async fn resolve_rh_external_sbom_descendants( sbom_external_sbom_id: Uuid, sbom_external_node_ref: String, connection: &C, -) -> Option { +) -> Result, Error> { // find checksum value for the node - match sbom_node_checksum::Entity::find() + + let Some(entity) = sbom_node_checksum::Entity::find() .filter(sbom_node_checksum::Column::NodeId.eq(sbom_external_node_ref.clone())) .filter(sbom_node_checksum::Column::SbomId.eq(sbom_external_sbom_id)) .one(connection) - .await - { - Ok(Some(entity)) => { - // now find if there are any other nodes with the same checksums - match sbom_node_checksum::Entity::find() - .filter(sbom_node_checksum::Column::Value.eq(entity.value.to_string())) - .filter(sbom_node_checksum::Column::SbomId.ne(entity.sbom_id)) - .all(connection) - .await - { - Ok(matches) => matches - .into_iter() - // The use of .find here ensures we never match on cdx top level metadata component - // which has not defined a bom-ref - we can 'sniff' this because such nodes always - // are ingested with a uuid node-id. - .find(|model| Uuid::parse_str(&model.node_id).is_err()) - .map(|matched_model| ResolvedSbom { - sbom_id: matched_model.sbom_id, - node_id: matched_model.node_id, - }), - _ => None, + .await? + else { + log::debug!("Unable to find checksum"); + return Ok(None); + }; + + log::debug!("Checksum: {} / {}", entity.value, entity.sbom_id); + + // now find if there are any other nodes with the same checksums + let matches = sbom_node_checksum::Entity::find() + .filter(sbom_node_checksum::Column::Value.eq(entity.value.to_string())) + .filter(sbom_node_checksum::Column::SbomId.ne(entity.sbom_id)) + .all(connection) + .await?; + + log::debug!("Found {} nodes by checksum", matches.len()); + + Ok(matches + .into_iter() + // The use of .find here ensures we never match on cdx top level metadata component + // which has not defined a bom-ref - we can 'sniff' this because such nodes always + // are ingested with a uuid node-id. + .find(|model| { + if Uuid::parse_str(&model.node_id).is_err() { + // failed to parse, we keep it + true + } else { + log::debug!("Dropping suspected top-level node ID: {}", model.node_id); + false } - } - _ => None, - } + }) + .map(|matched_model| ResolvedSbom { + sbom_id: matched_model.sbom_id, + node_id: matched_model.node_id, + })) } async fn resolve_rh_external_sbom_ancestors( sbom_external_sbom_id: Uuid, sbom_external_node_ref: String, connection: &C, -) -> Vec { +) -> Result, Error> { // find related checksum value(s) for the node, because any single component can be referred to by multiple // sboms, this function returns a Vec. - match sbom_node_checksum::Entity::find() - .filter(sbom_node_checksum::Column::NodeId.eq(sbom_external_node_ref.clone())) - .filter(sbom_node_checksum::Column::SbomId.eq(sbom_external_sbom_id)) - .one(connection) - .await - { - Ok(Some(entity)) => { - // now find if there are any other nodes with the same checksums - match sbom_node_checksum::Entity::find() - .filter(sbom_node_checksum::Column::Value.eq(entity.value.to_string())) - .filter(sbom_node_checksum::Column::SbomId.ne(entity.sbom_id)) - .all(connection) - .await - { - Ok(matches) => matches + Ok( + match sbom_node_checksum::Entity::find() + .filter(sbom_node_checksum::Column::NodeId.eq(sbom_external_node_ref.clone())) + .filter(sbom_node_checksum::Column::SbomId.eq(sbom_external_sbom_id)) + .one(connection) + .await? + { + Some(entity) => { + // now find if there are any other nodes with the same checksums + sbom_node_checksum::Entity::find() + .filter(sbom_node_checksum::Column::Value.eq(entity.value.to_string())) + .filter(sbom_node_checksum::Column::SbomId.ne(entity.sbom_id)) + .all(connection) + .await? .into_iter() .map(|matched| ResolvedSbom { sbom_id: matched.sbom_id, node_id: matched.node_id, }) - .collect(), - _ => vec![], + .collect() } - } - _ => { - vec![] - } - } + _ => vec![], + }, + ) } impl AnalysisService { @@ -426,10 +444,10 @@ impl AnalysisService { graphs: &'g [(String, Arc)], concurrency: usize, create: F, - ) -> Vec + ) -> Result, Error> where F: Fn(&'g Graph, NodeIndex, &'g graph::Node) -> Fut + Clone, - Fut: Future, + Fut: Future>, { let query = query.into(); @@ -449,7 +467,7 @@ impl AnalysisService { .map(move |(node_index, package_node)| create(graph, node_index, package_node)) }) .buffer_unordered(concurrency) - .collect::>() + .try_collect::>() .await } @@ -461,7 +479,7 @@ impl AnalysisService { graphs: &[(String, Arc)], connection: &C, graph_cache: Arc, - ) -> Vec { + ) -> Result, Error> { let relationships = options.relationships; log::debug!("relations: {:?}", relationships); @@ -507,12 +525,12 @@ impl AnalysisService { let (ancestors, descendants) = futures::join!(ancestors, descendants); - Node { + Ok(Node { base: node.into(), relationship: None, - ancestors, - descendants, - } + ancestors: ancestors?, + descendants: descendants?, + }) } }, ) @@ -544,7 +562,7 @@ impl AnalysisService { connection, self.inner.graph_cache.clone(), ) - .await; + .await?; Ok(paginated.paginate_array(&components)) } @@ -571,7 +589,7 @@ impl AnalysisService { connection, self.inner.graph_cache.clone(), ) - .await; + .await?; Ok(paginated.paginate_array(&components)) } @@ -603,7 +621,7 @@ impl AnalysisService { connection, self.inner.graph_cache.clone(), ) - .await; + .await?; Ok(paginated.paginate_array(&components)) } diff --git a/modules/analysis/src/service/test.rs b/modules/analysis/src/service/test.rs index 01fbbab72..77183d5eb 100644 --- a/modules/analysis/src/service/test.rs +++ b/modules/analysis/src/service/test.rs @@ -754,12 +754,12 @@ async fn resolve_sbom_cdx_external_node_sbom(ctx: &TrustifyContext) -> Result<() // ingest cdx example ctx.ingest_document("cyclonedx/simple-ext-a.json").await?; let get_external_sbom = - resolve_external_sbom("urn:cdx:a4f16b62-fea9-42c1-8365-d72d3cef37d1/2#a", &ctx.db).await; + resolve_external_sbom("urn:cdx:a4f16b62-fea9-42c1-8365-d72d3cef37d1/2#a", &ctx.db).await?; assert_eq!(get_external_sbom, None); // now ingest cdx sbom referred in "cyclonedx/simple-ext-b.json" ctx.ingest_document("cyclonedx/simple-ext-b.json").await?; let get_external_sbom = - resolve_external_sbom("urn:cdx:a4f16b62-fea9-42c1-8365-d72d3cef37d1/2#a", &ctx.db).await; + resolve_external_sbom("urn:cdx:a4f16b62-fea9-42c1-8365-d72d3cef37d1/2#a", &ctx.db).await?; assert!(get_external_sbom.is_some()); if let Some(ResolvedSbom { sbom_id: _, @@ -778,11 +778,11 @@ async fn resolve_sbom_cdx_external_node_sbom(ctx: &TrustifyContext) -> Result<() async fn resolve_sbom_spdx_external_node_sbom(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { // now try spdx example ctx.ingest_document("spdx/simple-ext-a.json").await?; - let get_external_sbom = resolve_external_sbom("DocumentRef-ext-b:SPDXRef-A", &ctx.db).await; + let get_external_sbom = resolve_external_sbom("DocumentRef-ext-b:SPDXRef-A", &ctx.db).await?; assert_eq!(get_external_sbom, None); // now ingest spdx sbom referred in "spdx/simple-ext-b.json" ctx.ingest_document("spdx/simple-ext-b.json").await?; - let get_external_sbom = resolve_external_sbom("DocumentRef-ext-b:SPDXRef-A", &ctx.db).await; + let get_external_sbom = resolve_external_sbom("DocumentRef-ext-b:SPDXRef-A", &ctx.db).await?; assert!(get_external_sbom.is_some()); if let Some(ResolvedSbom { sbom_id: _, @@ -808,7 +808,7 @@ async fn resolve_sbom_spdx_rh_variant_external_node_sbom( "SPDXRef-RHEL-9.2-EUS:SPDXRef-openssl-3.0.7-18.el9-2", &ctx.db, ) - .await; + .await?; assert_eq!(get_external_sbom, None); // now ingest rh component spdx "spdx/rh/product_component/openssl-3.0.7-18.el9_2.spdx.json" @@ -818,7 +818,7 @@ async fn resolve_sbom_spdx_rh_variant_external_node_sbom( "SPDXRef-RHEL-9.2-EUS:SPDXRef-openssl-3.0.7-18.el9-2", &ctx.db, ) - .await; + .await?; if let Some(ResolvedSbom { sbom_id: external_sbom_id, @@ -853,7 +853,7 @@ async fn resolve_sbom_cdx_rh_variant_external_node_sbom( "Red Hat Enterprise Linux 9.2 EUS:pkg:rpm/redhat/openssl@3.0.7-18.el9_2?arch=src", &ctx.db, ) - .await; + .await?; assert_eq!(get_external_sbom, None); // now ingest rh component spdx "cyclonedx/rh/product_component/openssl-3.0.7-18.el9_2.cdx.json" @@ -863,7 +863,7 @@ async fn resolve_sbom_cdx_rh_variant_external_node_sbom( "Red Hat Enterprise Linux 9.2 EUS:pkg:rpm/redhat/openssl@3.0.7-18.el9_2?arch=src", &ctx.db, ) - .await; + .await?; if let Some(ResolvedSbom { sbom_id: external_sbom_id, From 891dac51fe774f77f6c51c4a7e02a45ae4427518 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Wed, 19 Nov 2025 11:33:58 +0100 Subject: [PATCH 4/7] chore: switch to test_context 0.5 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- rust-toolchain.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8ba29dbd..a289ed7d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7712,9 +7712,9 @@ dependencies = [ [[package]] name = "test-context" -version = "0.4.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb69cce03e432993e2dc1f93f7899b952300fcb6dc44191a1b830b60b8c3c8aa" +checksum = "7d94db16dc1c321805ce55f286c4023fa58a2c9c742568f95c5cfe2e95d250d7" dependencies = [ "futures", "test-context-macros", @@ -7722,9 +7722,9 @@ dependencies = [ [[package]] name = "test-context-macros" -version = "0.4.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97e0639209021e54dbe19cafabfc0b5574b078c37358945e6d473eabe39bb974" +checksum = "aabcca9d2cad192cfe258cd3562b7584516191a5c9b6a0002a6bb8b75ee7d21d" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7959a9090..c994b83f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,7 +129,7 @@ strum = "0.27.1" tar = "0.4.43" temp-env = "0.3" tempfile = "3" -test-context = "0.4" +test-context = "0.5" test-log = "0.2.16" thiserror = "2" time = "0.3" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bd44fa65b..6e3d8428d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.90.0" +channel = "1.91.0" components = [ "rustfmt", "clippy" ] From dcfcb629bef116f4a58901fcdf67ad304e130c92 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Wed, 19 Nov 2025 11:34:31 +0100 Subject: [PATCH 5/7] test: restructure tests to use rstest --- Cargo.lock | 1 + modules/analysis/Cargo.toml | 7 +- .../src/endpoints/tests/latest_filters.rs | 357 ++++++++---------- 3 files changed, 161 insertions(+), 204 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a289ed7d4..4e23e6e31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8428,6 +8428,7 @@ dependencies = [ "packageurl", "parking_lot 0.12.5", "petgraph 0.8.3", + "rstest", "sea-orm", "sea-query", "serde", diff --git a/modules/analysis/Cargo.toml b/modules/analysis/Cargo.toml index ed37a8a81..f69723908 100644 --- a/modules/analysis/Cargo.toml +++ b/modules/analysis/Cargo.toml @@ -41,20 +41,21 @@ actix-http = { workspace = true } bytes = { workspace = true } bytesize = { workspace = true } chrono = { workspace = true } +csaf = { workspace = true } hex = { workspace = true } humantime = { workspace = true } itertools = { workspace = true } jsonpath-rust = { workspace = true } log = { workspace = true } +packageurl = { workspace = true } petgraph = { workspace = true } +rstest = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } test-context = { workspace = true } test-log = { workspace = true, features = ["log", "trace"] } -tokio-util = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] } +tokio-util = { workspace = true, features = ["full"] } trustify-test-context = { workspace = true } urlencoding = { workspace = true } -csaf = { workspace = true } -packageurl = { workspace = true } zip = { workspace = true } diff --git a/modules/analysis/src/endpoints/tests/latest_filters.rs b/modules/analysis/src/endpoints/tests/latest_filters.rs index 3645ec1d9..1b455994f 100644 --- a/modules/analysis/src/endpoints/tests/latest_filters.rs +++ b/modules/analysis/src/endpoints/tests/latest_filters.rs @@ -1,14 +1,38 @@ use super::req::*; use crate::test::caller; +use rstest::*; use serde_json::{Value, json}; use test_context::test_context; -use test_log::test; use trustify_test_context::{TrustifyContext, subset::ContainsSubset}; #[test_context(TrustifyContext)] -#[test(actix_web::test)] +#[rstest] +#[case( // cpe search + Req { loc: Loc::Id("cpe:/a:redhat:quay:3::el8"), ..Req::default() }, + 2 +)] +#[case( // cpe latest search + Req { loc: Loc::Id("cpe:/a:redhat:quay:3::el8"), latest: true, ..Req::default() }, + 1 +)] +#[case( // purl partial search + Req { loc: Loc::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), ancestors: Some(10), ..Req::default() }, + 18 +)] +#[case( // purl partial search latest + Req { loc: Loc::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), ancestors: Some(10), latest: true, ..Req::default() }, + 2 +)] +#[case( // purl partial search latest + Req { loc: Loc::Q("purl:name~quay-builder-qemu-rhcos-rhel8&purl:ty=oci"), ancestors: Some(10), latest: true, ..Req::default() }, + 7 +)] +#[test_log::test(actix_web::test)] async fn resolve_rh_variant_latest_filter_container_cdx( ctx: &TrustifyContext, + #[case] req: Req<'_>, + #[case] total: usize, + #[values(false, true)] prime_cache: bool, ) -> Result<(), anyhow::Error> { let app = caller(ctx).await?; @@ -22,66 +46,54 @@ async fn resolve_rh_variant_latest_filter_container_cdx( ]) .await?; - let _response = app.req(Req::default()).await?; - - // cpe search - let response = app - .req(Req { - loc: Loc::Id("cpe:/a:redhat:quay:3::el8"), - ..Req::default() - }) - .await?; - assert_eq!(2, response["total"]); - - // cpe latest search - let response = app - .req(Req { - latest: true, - loc: Loc::Id("cpe:/a:redhat:quay:3::el8"), - ..Req::default() - }) - .await?; - assert_eq!(1, response["total"]); - - // purl partial search - let response = app - .req(Req { - loc: Loc::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), - ancestors: Some(10), - ..Req::default() - }) - .await?; - assert_eq!(18, response["total"]); + if prime_cache { + let _response = app.req(Req::default()).await?; + } - // purl partial search latest - let response = app - .req(Req { - latest: true, - loc: Loc::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), - ancestors: Some(10), - ..Req::default() - }) - .await?; - assert_eq!(2, response["total"]); + let response = app.req(req).await?; - // purl partial search latest - let response = app - .req(Req { - latest: true, - loc: Loc::Q("purl:name~quay-builder-qemu-rhcos-rhel8&purl:ty=oci"), - ancestors: Some(10), - ..Req::default() - }) - .await?; - assert_eq!(7, response["total"]); + log::info!("{response:#?}"); + assert_eq!(total, response["total"]); Ok(()) } #[test_context(TrustifyContext)] -#[test(actix_web::test)] +#[rstest] +#[case( // cpe search + Req { loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), ..Req::default() }, + 2 +)] +#[case( // cpe latest search + Req { loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), latest: true, ..Req::default() }, + 1 +)] +#[case( // purl partial search + Req { loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm"), ancestors: Some(10), ..Req::default() }, + 30 +)] +#[case( // purl partial latest search + Req { loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm"), ancestors: Some(10), latest: true, ..Req::default() }, + 15 +)] +#[case( // purl more specific latest q search + Req { loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm-devel@"), latest: true, ..Req::default() }, + 5 +)] +#[case( // name exact search + Req { loc: Loc::Id("NetworkManager-libnm-devel"), ..Req::default() }, + 10 +)] +#[case( // latest name exact search + Req { loc: Loc::Id("NetworkManager-libnm-devel"), latest: true, ..Req::default() }, + 5 +)] +#[test_log::test(actix_web::test)] async fn resolve_rh_variant_latest_filter_rpms_cdx( ctx: &TrustifyContext, + #[case] req: Req<'_>, + #[case] total: usize, + #[values(false, true)] prime_cache: bool, ) -> Result<(), anyhow::Error> { let app = caller(ctx).await?; @@ -93,83 +105,50 @@ async fn resolve_rh_variant_latest_filter_rpms_cdx( ]) .await?; - let _response = app.req(Req::default()).await?; - - // cpe search - let response = app - .req(Req { - loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 2); - - // cpe latest search - let response = app - .req(Req { - latest: true, - loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 1); - - // purl partial search - let response = app - .req(Req { - loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm"), - ancestors: Some(10), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 30); - - // purl partial latest search - let response = app - .req(Req { - latest: true, - loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 15); - - // purl more specific latest q search - let response = app - .req(Req { - latest: true, - loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm-devel@"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 5); + if prime_cache { + let _response = app.req(Req::default()).await?; + } - // name exact search - let response = app - .req(Req { - loc: Loc::Id("NetworkManager-libnm-devel"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 10); + let response = app.req(req).await?; - // latest name exact search - let response = app - .req(Req { - latest: true, - loc: Loc::Id("NetworkManager-libnm-devel"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 5); + log::info!("{response:#?}"); + assert_eq!(total, response["total"]); Ok(()) } #[test_context(TrustifyContext)] -#[test(actix_web::test)] +#[rstest] +#[case( // cpe search + Req { loc: Loc::Id("cpe:/a:redhat:camel_quarkus:3"), ..Req::default() }, + 2 +)] +#[case( // cpe latest search + Req { loc: Loc::Id("cpe:/a:redhat:camel_quarkus:3"), latest: true, ..Req::default() }, + 1 +)] +#[case( // purl partial search + Req { loc: Loc::Q("pkg:maven/io.vertx/vertx-core@"), ancestors: Some(10), ..Req::default() }, + 6 +)] +#[case( // purl partial latest search + Req { loc: Loc::Q("pkg:maven/io.vertx/vertx-core@"), latest: true, ..Req::default() }, + 2 +)] +#[case( // name exact search + Req { loc: Loc::Id("vertx-core"), ..Req::default() }, + 6 +)] +#[case( // latest name exact search + Req { loc: Loc::Id("vertx-core"), latest: true, ..Req::default() }, + 2 +)] +#[test_log::test(actix_web::test)] async fn resolve_rh_variant_latest_filter_middleware_cdx( ctx: &TrustifyContext, + #[case] req: Req<'_>, + #[case] total: usize, + #[values(false, true)] prime_cache: bool, ) -> Result<(), anyhow::Error> { let app = caller(ctx).await?; @@ -183,72 +162,25 @@ async fn resolve_rh_variant_latest_filter_middleware_cdx( ]) .await?; - let _response = app.req(Req::default()).await?; - - // cpe search - let response = app - .req(Req { - loc: Loc::Id("cpe:/a:redhat:camel_quarkus:3"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 2); - - // cpe latest search - let response = app - .req(Req { - latest: true, - loc: Loc::Id("cpe:/a:redhat:camel_quarkus:3"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 1); - - // purl partial search - let response = app - .req(Req { - loc: Loc::Q("pkg:maven/io.vertx/vertx-core@"), - ancestors: Some(10), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 6); + if prime_cache { + let _response = app.req(Req::default()).await?; + } - // purl partial latest search - let response = app - .req(Req { - latest: true, - loc: Loc::Q("pkg:maven/io.vertx/vertx-core@"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 2); + let response = app.req(req).await?; - // name exact search - let response = app - .req(Req { - loc: Loc::Id("vertx-core"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 6); - - // latest name exact search - let response = app - .req(Req { - latest: true, - loc: Loc::Id("vertx-core"), - ..Req::default() - }) - .await?; - assert_eq!(response["total"], 2); + log::info!("{response:#?}"); + assert_eq!(total, response["total"]); Ok(()) } #[test_context(TrustifyContext)] -#[test(actix_web::test)] -async fn test_tc2606(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { +#[rstest] +#[test_log::test(actix_web::test)] +async fn test_tc2606( + ctx: &TrustifyContext, + #[values(false, true)] prime_cache: bool, +) -> Result<(), anyhow::Error> { let app = caller(ctx).await?; ctx.ingest_documents([ @@ -261,7 +193,9 @@ async fn test_tc2606(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ]) .await?; - let _response = app.req(Req::default()).await?; + if prime_cache { + let _response = app.req(Req::default()).await?; + } // latest cpe search let response = app @@ -273,7 +207,6 @@ async fn test_tc2606(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { }) .await?; log::info!("{response:#?}"); - assert_eq!(response["total"], 2); assert!(response.contains_subset(json!( { @@ -337,14 +270,19 @@ async fn test_tc2606(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ], } ], - "total": 2 }))); + assert_eq!(response["total"], 2); + Ok(()) } #[test_context(TrustifyContext)] -#[test(actix_web::test)] -async fn test_tc2677(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { +#[rstest] +#[test_log::test(actix_web::test)] +async fn test_tc2677( + ctx: &TrustifyContext, + #[values(false, true)] prime_cache: bool, +) -> Result<(), anyhow::Error> { let app = caller(ctx).await?; ctx.ingest_documents([ @@ -354,7 +292,9 @@ async fn test_tc2677(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ]) .await?; - let _response = app.req(Req::default()).await?; + if prime_cache { + let _response = app.req(Req::default()).await?; + } // latest cpe search let response = app @@ -422,8 +362,22 @@ async fn test_tc2677(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { } #[test_context(TrustifyContext)] -#[test(actix_web::test)] -async fn test_tc2717(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { +#[rstest] +#[case( // non-latest + Req { loc: Loc::Id("pkg:maven/io.vertx/vertx-core"), ..Req::default() }, + 0 +)] +#[case( // latest + Req { loc: Loc::Id("pkg:maven/io.vertx/vertx-core"), latest: true, ..Req::default() }, + 2 +)] +#[test_log::test(actix_web::test)] +async fn test_tc2717( + ctx: &TrustifyContext, + #[case] req: Req<'_>, + #[case] total: usize, + #[values(false, true)] prime_cache: bool, +) -> Result<(), anyhow::Error> { let app = caller(ctx).await?; ctx.ingest_documents([ @@ -432,24 +386,23 @@ async fn test_tc2717(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ]) .await?; - let _response = app.req(Req::default()).await?; - - let response = app - .req(Req { - latest: true, - loc: Loc::Id("pkg:maven/io.vertx/vertx-core"), - ..Req::default() - }) - .await?; + if prime_cache { + let _response = app.req(Req::default()).await?; + } - assert_eq!(response["total"], 2); + let response = app.req(req).await?; + assert_eq!(total, response["total"]); Ok(()) } #[test_context(TrustifyContext)] -#[test(actix_web::test)] -async fn test_tc2578(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { +#[rstest] +#[test_log::test(actix_web::test)] +async fn test_tc2578( + ctx: &TrustifyContext, + #[values(false, true)] prime_cache: bool, +) -> Result<(), anyhow::Error> { let app = caller(ctx).await?; ctx.ingest_documents([ @@ -465,7 +418,9 @@ async fn test_tc2578(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ]) .await?; - let _response = app.req(Req::default()).await?; + if prime_cache { + let _response = app.req(Req::default()).await?; + } let response = app .req(Req { From f158b9411b5361278cc24eb749eeaeca650a9a18 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 21 Nov 2025 09:23:28 +0100 Subject: [PATCH 6/7] chore: make clippy happy --- modules/analysis/src/endpoints/tests/latest_filters.rs | 2 +- modules/analysis/src/service/collector.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/analysis/src/endpoints/tests/latest_filters.rs b/modules/analysis/src/endpoints/tests/latest_filters.rs index 1b455994f..4c1e2acb0 100644 --- a/modules/analysis/src/endpoints/tests/latest_filters.rs +++ b/modules/analysis/src/endpoints/tests/latest_filters.rs @@ -1,7 +1,7 @@ use super::req::*; use crate::test::caller; use rstest::*; -use serde_json::{Value, json}; +use serde_json::json; use test_context::test_context; use trustify_test_context::{TrustifyContext, subset::ContainsSubset}; diff --git a/modules/analysis/src/service/collector.rs b/modules/analysis/src/service/collector.rs index 2b5ee2ec0..b9c9c4458 100644 --- a/modules/analysis/src/service/collector.rs +++ b/modules/analysis/src/service/collector.rs @@ -247,10 +247,10 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> { // recurse into those external sbom nodes and save - Ok(collector + collector .with(external_graph.as_ref(), external_node_index) .collect_graph() - .await?) + .await } }) .buffer_unordered(self.concurrency) From 27a92d82e062d5e1b50496c5005f11ceb92f32dc Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Fri, 21 Nov 2025 12:08:53 +0100 Subject: [PATCH 7/7] refactor: makes changes requested from PR review --- .../src/endpoints/tests/latest_filters.rs | 46 +++++++++---------- modules/analysis/src/endpoints/tests/req.rs | 33 ++++++++----- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/modules/analysis/src/endpoints/tests/latest_filters.rs b/modules/analysis/src/endpoints/tests/latest_filters.rs index 4c1e2acb0..8c155fb97 100644 --- a/modules/analysis/src/endpoints/tests/latest_filters.rs +++ b/modules/analysis/src/endpoints/tests/latest_filters.rs @@ -8,23 +8,23 @@ use trustify_test_context::{TrustifyContext, subset::ContainsSubset}; #[test_context(TrustifyContext)] #[rstest] #[case( // cpe search - Req { loc: Loc::Id("cpe:/a:redhat:quay:3::el8"), ..Req::default() }, + Req { what: What::Id("cpe:/a:redhat:quay:3::el8"), ..Req::default() }, 2 )] #[case( // cpe latest search - Req { loc: Loc::Id("cpe:/a:redhat:quay:3::el8"), latest: true, ..Req::default() }, + Req { what: What::Id("cpe:/a:redhat:quay:3::el8"), latest: true, ..Req::default() }, 1 )] #[case( // purl partial search - Req { loc: Loc::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), ancestors: Some(10), ..Req::default() }, + Req { what: What::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), ancestors: Some(10), ..Req::default() }, 18 )] #[case( // purl partial search latest - Req { loc: Loc::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), ancestors: Some(10), latest: true, ..Req::default() }, + Req { what: What::Q("pkg:oci/quay-builder-qemu-rhcos-rhel8"), ancestors: Some(10), latest: true, ..Req::default() }, 2 )] #[case( // purl partial search latest - Req { loc: Loc::Q("purl:name~quay-builder-qemu-rhcos-rhel8&purl:ty=oci"), ancestors: Some(10), latest: true, ..Req::default() }, + Req { what: What::Q("purl:name~quay-builder-qemu-rhcos-rhel8&purl:ty=oci"), ancestors: Some(10), latest: true, ..Req::default() }, 7 )] #[test_log::test(actix_web::test)] @@ -61,31 +61,31 @@ async fn resolve_rh_variant_latest_filter_container_cdx( #[test_context(TrustifyContext)] #[rstest] #[case( // cpe search - Req { loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), ..Req::default() }, + Req { what: What::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), ..Req::default() }, 2 )] #[case( // cpe latest search - Req { loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), latest: true, ..Req::default() }, + Req { what: What::Id("cpe:/a:redhat:rhel_eus:9.4::crb"), latest: true, ..Req::default() }, 1 )] #[case( // purl partial search - Req { loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm"), ancestors: Some(10), ..Req::default() }, + Req { what: What::Q("pkg:rpm/redhat/NetworkManager-libnm"), ancestors: Some(10), ..Req::default() }, 30 )] #[case( // purl partial latest search - Req { loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm"), ancestors: Some(10), latest: true, ..Req::default() }, + Req { what: What::Q("pkg:rpm/redhat/NetworkManager-libnm"), ancestors: Some(10), latest: true, ..Req::default() }, 15 )] #[case( // purl more specific latest q search - Req { loc: Loc::Q("pkg:rpm/redhat/NetworkManager-libnm-devel@"), latest: true, ..Req::default() }, + Req { what: What::Q("pkg:rpm/redhat/NetworkManager-libnm-devel@"), latest: true, ..Req::default() }, 5 )] #[case( // name exact search - Req { loc: Loc::Id("NetworkManager-libnm-devel"), ..Req::default() }, + Req { what: What::Id("NetworkManager-libnm-devel"), ..Req::default() }, 10 )] #[case( // latest name exact search - Req { loc: Loc::Id("NetworkManager-libnm-devel"), latest: true, ..Req::default() }, + Req { what: What::Id("NetworkManager-libnm-devel"), latest: true, ..Req::default() }, 5 )] #[test_log::test(actix_web::test)] @@ -120,27 +120,27 @@ async fn resolve_rh_variant_latest_filter_rpms_cdx( #[test_context(TrustifyContext)] #[rstest] #[case( // cpe search - Req { loc: Loc::Id("cpe:/a:redhat:camel_quarkus:3"), ..Req::default() }, + Req { what: What::Id("cpe:/a:redhat:camel_quarkus:3"), ..Req::default() }, 2 )] #[case( // cpe latest search - Req { loc: Loc::Id("cpe:/a:redhat:camel_quarkus:3"), latest: true, ..Req::default() }, + Req { what: What::Id("cpe:/a:redhat:camel_quarkus:3"), latest: true, ..Req::default() }, 1 )] #[case( // purl partial search - Req { loc: Loc::Q("pkg:maven/io.vertx/vertx-core@"), ancestors: Some(10), ..Req::default() }, + Req { what: What::Q("pkg:maven/io.vertx/vertx-core@"), ancestors: Some(10), ..Req::default() }, 6 )] #[case( // purl partial latest search - Req { loc: Loc::Q("pkg:maven/io.vertx/vertx-core@"), latest: true, ..Req::default() }, + Req { what: What::Q("pkg:maven/io.vertx/vertx-core@"), latest: true, ..Req::default() }, 2 )] #[case( // name exact search - Req { loc: Loc::Id("vertx-core"), ..Req::default() }, + Req { what: What::Id("vertx-core"), ..Req::default() }, 6 )] #[case( // latest name exact search - Req { loc: Loc::Id("vertx-core"), latest: true, ..Req::default() }, + Req { what: What::Id("vertx-core"), latest: true, ..Req::default() }, 2 )] #[test_log::test(actix_web::test)] @@ -201,7 +201,7 @@ async fn test_tc2606( let response = app .req(Req { latest: true, - loc: Loc::Id("cpe:/a:redhat:rhel_eus:9.4::appstream"), + what: What::Id("cpe:/a:redhat:rhel_eus:9.4::appstream"), descendants: Some(1), ..Req::default() }) @@ -300,7 +300,7 @@ async fn test_tc2677( let response = app .req(Req { latest: true, - loc: Loc::Id("cpe:/a:redhat:3scale:2.15::el9"), + what: What::Id("cpe:/a:redhat:3scale:2.15::el9"), descendants: Some(10), ..Req::default() }) @@ -364,11 +364,11 @@ async fn test_tc2677( #[test_context(TrustifyContext)] #[rstest] #[case( // non-latest - Req { loc: Loc::Id("pkg:maven/io.vertx/vertx-core"), ..Req::default() }, + Req { what: What::Id("pkg:maven/io.vertx/vertx-core"), ..Req::default() }, 0 )] #[case( // latest - Req { loc: Loc::Id("pkg:maven/io.vertx/vertx-core"), latest: true, ..Req::default() }, + Req { what: What::Id("pkg:maven/io.vertx/vertx-core"), latest: true, ..Req::default() }, 2 )] #[test_log::test(actix_web::test)] @@ -425,7 +425,7 @@ async fn test_tc2578( let response = app .req(Req { latest: true, - loc: Loc::Id("cpe:/a:redhat:jboss_enterprise_application_platform:7.4"), + what: What::Id("cpe:/a:redhat:jboss_enterprise_application_platform:7.4"), descendants: Some(100), ..Req::default() }) diff --git a/modules/analysis/src/endpoints/tests/req.rs b/modules/analysis/src/endpoints/tests/req.rs index 205befc29..5decb6be6 100644 --- a/modules/analysis/src/endpoints/tests/req.rs +++ b/modules/analysis/src/endpoints/tests/req.rs @@ -3,23 +3,33 @@ use actix_http::{Request, StatusCode}; use actix_web::test::TestRequest; use serde_json::Value; +/// Request on the analysis API #[derive(Default, Copy, Clone)] pub struct Req<'a> { + /// request against "latest" API pub latest: bool, + /// Level of ancestors to request pub ancestors: Option, + /// Level of descendants to request pub descendants: Option, - pub loc: Loc<'a>, + /// What is being requested + pub what: What<'a>, } +/// Indication of what is being requested #[derive(Default, Copy, Clone)] -pub enum Loc<'a> { +pub enum What<'a> { + /// Everything #[default] None, + /// Search by `q` parameter Q(&'a str), + /// By ID Id(&'a str), } pub trait ReqExt { + /// Process request async fn req(&self, req: Req<'_>) -> anyhow::Result; } @@ -27,7 +37,7 @@ impl ReqExt for C { async fn req(&self, req: Req<'_>) -> anyhow::Result { let Req { latest, - loc, + what: loc, ancestors, descendants, } = req; @@ -37,19 +47,18 @@ impl ReqExt for C { false => "", }; + const BASE: &str = "/api/v2/analysis/"; + let mut uri = match loc { - Loc::None => { - format!("/api/v2/analysis/{latest}component?",) + What::None => { + format!("{BASE}{latest}component?",) } - Loc::Q(q) => { - format!( - "/api/v2/analysis/{latest}component?q={q}&", - q = urlencoding::encode(q), - ) + What::Q(q) => { + format!("{BASE}{latest}component?q={q}&", q = urlencoding::encode(q),) } - Loc::Id(id) => { + What::Id(id) => { format!( - "/api/v2/analysis/{latest}component/{id}?", + "{BASE}{latest}component/{id}?", id = urlencoding::encode(id), ) }