Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests for spdx "relationshipType": "PACKAGE_OF" #1186

Merged
merged 3 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions modules/analysis/src/endpoints/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -654,28 +654,35 @@ 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)?);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really wish serde's impl of :#? used to_string_pretty. I can remember the former, but never the latter. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100%


let sbom = &response["items"][0];
let matches: Vec<_> = sbom["deps"]
.as_array()
.into_iter()
.flatten()
.filter(|m| {
m == &&json!({
"sbom_id": sbom["sbom_id"],
"node_id": "SPDXRef-83c9faa0-ca85-4e48-9165-707b2f9a324b",
assert!(response.contains_deep_subset(json!({
"items": [ {
"deps": [ {
"relationship": "PackageOf",
"purl": [],
"cpe": m["cpe"], // long list assume it's correct
"name": "SATELLITE-6.15-RHEL-8",
"version": "6.15",
"deps": [],
})
})
.collect();
}]
}]
})));

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::debug!("{}", serde_json::to_string_pretty(&response)?);

assert!(response.contains_deep_subset(json!({
"items": [ {
"ancestors": [ {
"relationship": "PackageOf",
"name": "rubygem-google-cloud-compute",
"version": "0.5.0-1.el8sat"
}]
}]
})));

Ok(())
}
1 change: 1 addition & 0 deletions test-context/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
90 changes: 90 additions & 0 deletions test-context/src/subset.rs
Original file line number Diff line number Diff line change
@@ -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))
}
Comment on lines +17 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary, because contains_deep_subset already does this. We want to be able to pass in an array and have it match explicitly, e.g. m.contains_subset(json!({"purl": [ "pkg:rpm/redhat/[email protected]?arch=src" ]}))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so that:

let actual = json!([{"a":1}]);
actual.contains_subset(json!([{"a":1, "b":2}])); // should return false

if we used contains_deep_subset then it would return true.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect the absence of b to cause that to return false. I'm not sure how that Value::Array branch would even come into play here. What am I missing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed:

        let actual = json!([{"a":1}]);
        assert!(!actual.contains_subset(json!([{"a":1, "b":2}])));
        assert!(!actual.contains_deep_subset(json!([{"a":1, "b":2}])));

Maybe add that to test_array_subset?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works as I'd expect, too:

        let actual = json!([{"a":1, "b": 2}]);
        assert!(!actual.contains_subset(json!([{"a":1}])));
        assert!(actual.contains_deep_subset(json!([{"a":1}])));

Maybe contains_partial_subset seems best in light of those?


_ => 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])));
Comment on lines +78 to +88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "strict" or "fuzzy" or "partial" are better descriptors than "deep". The crucial idea is that we need both a way to test for an explicit match and a way to ask "are all of these in the other one?"

I think this test is more accurate thusly:

Suggested change
// 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])));
let actual = json!([1, 2, 3]);
assert!(actual.contains_subset(json!([1, 2, 3])));
assert!(!actual.contains_subset(json!([2])));
assert!(actual.contains_deep_subset(json!([2])));
assert!(actual.contains_deep_subset(json!([1, 3])));
assert!(!actual.contains_deep_subset(json!([0])));

But again, I'm not sure "deep" is the right word. We should always recurse through a recursive structure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I'd be happy with any of those options.

}
}
Loading