From 63f860f75ece58baa3536112a0762734e236fea3 Mon Sep 17 00:00:00 2001 From: Hiram Chirino Date: Wed, 22 Jan 2025 08:12:03 -0500 Subject: [PATCH 1/3] Add tests for spdx "relationshipType": "PACKAGE_OF" Verify the relationship type shows up in `/api/v2/analysis/root-component` API calls. Part of issue #1140 Signed-off-by: Hiram Chirino --- modules/analysis/src/endpoints/test.rs | 30 +++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/modules/analysis/src/endpoints/test.rs b/modules/analysis/src/endpoints/test.rs index 1dff26155..089e106f4 100644 --- a/modules/analysis/src/endpoints/test.rs +++ b/modules/analysis/src/endpoints/test.rs @@ -654,7 +654,7 @@ async fn spdx_package_of(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { let uri = format!("/api/v2/analysis/dep/{}", urlencoding::encode(purl)); let request: Request = TestRequest::get().uri(&uri).to_request(); let response: Value = app.call_and_read_body_json(request).await; - log::debug!("{response:#?}"); + log::debug!("{}", serde_json::to_string_pretty(&response)?); let sbom = &response["items"][0]; let matches: Vec<_> = sbom["deps"] @@ -677,5 +677,33 @@ async fn spdx_package_of(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { assert_eq!(1, matches.len()); + let uri = format!( + "/api/v2/analysis/root-component?q={}", + urlencoding::encode("SATELLITE-6.15-RHEL-8") + ); + let request: Request = TestRequest::get().uri(&uri).to_request(); + let response: Value = app.call_and_read_body_json(request).await; + log::info!("{}", serde_json::to_string_pretty(&response)?); + + let sbom = &response["items"][0]; + let matches: Vec<_> = sbom["ancestors"] + .as_array() + .into_iter() + .flatten() + .filter(|m| { + m == &&json!({ + "sbom_id": sbom["sbom_id"], + "node_id": m["node_id"], + "relationship": "PackageOf", + "purl": m["purl"], // long list assume it's correct + "cpe": m["cpe"], // long list assume it's correct + "name": "rubygem-google-cloud-compute", + "version": "0.5.0-1.el8sat" + }) + }) + .collect(); + + assert_eq!(1, matches.len()); + Ok(()) } From c47dcaafa64909f92e21015892451fffdf9b1eec Mon Sep 17 00:00:00 2001 From: Hiram Chirino Date: Thu, 23 Jan 2025 14:34:45 -0500 Subject: [PATCH 2/3] Improve tests based on PR feedback. Added a ContainsSubset trait which allows you to Test if a value has a subset of elements/fields And also deep version which does it recursively. Signed-off-by: Hiram Chirino --- modules/analysis/src/endpoints/test.rs | 21 ++---- test-context/src/lib.rs | 1 + test-context/src/subset.rs | 90 ++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 test-context/src/subset.rs diff --git a/modules/analysis/src/endpoints/test.rs b/modules/analysis/src/endpoints/test.rs index 089e106f4..3502948d4 100644 --- a/modules/analysis/src/endpoints/test.rs +++ b/modules/analysis/src/endpoints/test.rs @@ -6,7 +6,7 @@ use serde_json::{json, Value}; use std::collections::HashMap; use test_context::test_context; use test_log::test; -use trustify_test_context::{call::CallService, TrustifyContext}; +use trustify_test_context::{call::CallService, subset::ContainsSubset, TrustifyContext}; #[test_context(TrustifyContext)] #[test(actix_web::test)] @@ -662,16 +662,11 @@ async fn spdx_package_of(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { .into_iter() .flatten() .filter(|m| { - m == &&json!({ - "sbom_id": sbom["sbom_id"], - "node_id": "SPDXRef-83c9faa0-ca85-4e48-9165-707b2f9a324b", + m.contains_subset(json!({ "relationship": "PackageOf", - "purl": [], - "cpe": m["cpe"], // long list assume it's correct "name": "SATELLITE-6.15-RHEL-8", "version": "6.15", - "deps": [], - }) + })) }) .collect(); @@ -683,7 +678,7 @@ async fn spdx_package_of(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ); let request: Request = TestRequest::get().uri(&uri).to_request(); let response: Value = app.call_and_read_body_json(request).await; - log::info!("{}", serde_json::to_string_pretty(&response)?); + log::debug!("{}", serde_json::to_string_pretty(&response)?); let sbom = &response["items"][0]; let matches: Vec<_> = sbom["ancestors"] @@ -691,15 +686,11 @@ async fn spdx_package_of(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { .into_iter() .flatten() .filter(|m| { - m == &&json!({ - "sbom_id": sbom["sbom_id"], - "node_id": m["node_id"], + m.contains_subset(json!({ "relationship": "PackageOf", - "purl": m["purl"], // long list assume it's correct - "cpe": m["cpe"], // long list assume it's correct "name": "rubygem-google-cloud-compute", "version": "0.5.0-1.el8sat" - }) + })) }) .collect(); diff --git a/test-context/src/lib.rs b/test-context/src/lib.rs index 7bdef6943..736d753ca 100644 --- a/test-context/src/lib.rs +++ b/test-context/src/lib.rs @@ -4,6 +4,7 @@ pub mod app; pub mod auth; pub mod call; pub mod spdx; +pub mod subset; use futures::Stream; use peak_alloc::PeakAlloc; diff --git a/test-context/src/subset.rs b/test-context/src/subset.rs new file mode 100644 index 000000000..08f00de94 --- /dev/null +++ b/test-context/src/subset.rs @@ -0,0 +1,90 @@ +use serde_json::Value; + +pub trait ContainsSubset { + // Returns true if the value is a subset of the receiver. + fn contains_subset(&self, value: Value) -> bool; + // Returns true if the value a deep subset of the receiver. + fn contains_deep_subset(&self, value: Value) -> bool; +} + +impl ContainsSubset for Value { + fn contains_subset(&self, value: Value) -> bool { + match (self, &value) { + (Value::Object(src), Value::Object(subset)) => subset + .iter() + .all(|(k, v)| src.get(k).is_some_and(|x| x == v)), + + (Value::Array(src), Value::Array(subset)) => { + subset.iter().all(|v| src.iter().any(|x| x == v)) + } + + _ => value == *self, + } + } + + fn contains_deep_subset(&self, subset: Value) -> bool { + match (self, &subset) { + (Value::Object(src), Value::Object(tgt)) => tgt.iter().all(|(k, v)| { + src.get(k) + .is_some_and(|x| x.contains_deep_subset(v.clone())) + }), + + (Value::Array(src), Value::Array(subset)) => subset + .iter() + .all(|v| src.iter().any(|x| x.contains_deep_subset(v.clone()))), + + _ => subset == *self, + } + } +} + +#[cfg(test)] +mod test { + use crate::subset::ContainsSubset; + use serde_json::json; + + #[test] + fn test_is_subset() { + // actual can have additional fields + let actual = json!({ + "relationship": "PackageOf", + "other": "test", + }); + assert!(actual.contains_subset(json!({ + "relationship": "PackageOf", + }))); + + // case where an expected field does not match + let actual = json!({ + "relationship": "PackageOf", + "other": "test", + }); + assert!(!actual.contains_subset(json!({ + "relationship": "bad", + }))); + + // case where an expected field is missing + let actual = json!({ + "relationship": "PackageOf", + "other": "test", + }); + assert!(!actual.contains_subset(json!({ + "name": "SATELLITE-6.15-RHEL-8", + }))); + } + + #[test] + fn test_array_subset() { + // actual can have additional fields + let actual = json!([1, 2, 3]); + assert!(actual.contains_subset(json!([2]))); + + // other values can be interleaved. + let actual = json!([1, 2, 3]); + assert!(actual.contains_subset(json!([1, 3]))); + + // case where a value is missing + let actual = json!([1, 2, 3]); + assert!(!actual.contains_subset(json!([0]))); + } +} From 290d374c953c4b16686abfb32219e5659bfbf454 Mon Sep 17 00:00:00 2001 From: Hiram Chirino Date: Fri, 24 Jan 2025 13:37:32 -0500 Subject: [PATCH 3/3] Fix failing test. Signed-off-by: Hiram Chirino --- modules/analysis/src/endpoints/test.rs | 42 +++++++++----------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/modules/analysis/src/endpoints/test.rs b/modules/analysis/src/endpoints/test.rs index 3502948d4..6c922b29c 100644 --- a/modules/analysis/src/endpoints/test.rs +++ b/modules/analysis/src/endpoints/test.rs @@ -656,21 +656,15 @@ async fn spdx_package_of(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { let response: Value = app.call_and_read_body_json(request).await; log::debug!("{}", serde_json::to_string_pretty(&response)?); - let sbom = &response["items"][0]; - let matches: Vec<_> = sbom["deps"] - .as_array() - .into_iter() - .flatten() - .filter(|m| { - m.contains_subset(json!({ + assert!(response.contains_deep_subset(json!({ + "items": [ { + "deps": [ { "relationship": "PackageOf", "name": "SATELLITE-6.15-RHEL-8", "version": "6.15", - })) - }) - .collect(); - - assert_eq!(1, matches.len()); + }] + }] + }))); let uri = format!( "/api/v2/analysis/root-component?q={}", @@ -680,21 +674,15 @@ async fn spdx_package_of(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { let response: Value = app.call_and_read_body_json(request).await; log::debug!("{}", serde_json::to_string_pretty(&response)?); - let sbom = &response["items"][0]; - let matches: Vec<_> = sbom["ancestors"] - .as_array() - .into_iter() - .flatten() - .filter(|m| { - m.contains_subset(json!({ - "relationship": "PackageOf", - "name": "rubygem-google-cloud-compute", - "version": "0.5.0-1.el8sat" - })) - }) - .collect(); - - assert_eq!(1, matches.len()); + assert!(response.contains_deep_subset(json!({ + "items": [ { + "ancestors": [ { + "relationship": "PackageOf", + "name": "rubygem-google-cloud-compute", + "version": "0.5.0-1.el8sat" + }] + }] + }))); Ok(()) }