@@ -201,6 +201,143 @@ def test_not_same_1(self) -> None:
201201 self .assertNotEqual (hash (ce_1 ), hash (ce_2 ))
202202 self .assertFalse (ce_1 == ce_2 )
203203
204+ def test_identity_deserialization_single_dict_format (self ) -> None :
205+ """Test deserialization of identity field as a single dict (CycloneDX 1.5 format)"""
206+ # This is the format that was failing before the fix
207+ json_data = {
208+ 'identity' : {
209+ 'field' : 'name' ,
210+ 'confidence' : 1.0 ,
211+ 'concludedValue' : 'test-component'
212+ }
213+ }
214+ ce = ComponentEvidence .from_json (json_data ) # type: ignore[attr-defined]
215+ self .assertEqual (len (ce .identity ), 1 )
216+ identity = list (ce .identity )[0 ]
217+ self .assertEqual (identity .field , IdentityField .NAME )
218+ self .assertEqual (identity .confidence , Decimal ('1.0' ))
219+ self .assertEqual (identity .concluded_value , 'test-component' )
220+
221+ def test_identity_deserialization_array_format (self ) -> None :
222+ """Test deserialization of identity field as an array (CycloneDX 1.6 format)"""
223+ json_data = {
224+ 'identity' : [
225+ {
226+ 'field' : 'name' ,
227+ 'confidence' : 1.0 ,
228+ 'concludedValue' : 'test-component'
229+ },
230+ {
231+ 'field' : 'version' ,
232+ 'confidence' : 0.8 ,
233+ 'concludedValue' : '1.0.0'
234+ }
235+ ]
236+ }
237+ ce = ComponentEvidence .from_json (json_data ) # type: ignore[attr-defined]
238+ self .assertEqual (len (ce .identity ), 2 )
239+
240+ # Check that both identities are present
241+ identities = sorted (ce .identity , key = lambda x : x .field .value )
242+ self .assertEqual (identities [0 ].field , IdentityField .NAME )
243+ self .assertEqual (identities [0 ].concluded_value , 'test-component' )
244+ self .assertEqual (identities [1 ].field , IdentityField .VERSION )
245+ self .assertEqual (identities [1 ].concluded_value , '1.0.0' )
246+
247+ def test_identity_dict_format_converts_to_array_internally (self ) -> None :
248+ """Test that single dict identity format is converted to array format internally"""
249+ # When deserializing a single dict, it should be normalized to array format
250+ # before being passed to ComponentEvidence
251+ json_data_dict = {
252+ 'identity' : {
253+ 'field' : 'name' ,
254+ 'confidence' : 1.0 ,
255+ 'concludedValue' : 'test-component'
256+ }
257+ }
258+
259+ json_data_array = {
260+ 'identity' : [
261+ {
262+ 'field' : 'name' ,
263+ 'confidence' : 1.0 ,
264+ 'concludedValue' : 'test-component'
265+ }
266+ ]
267+ }
268+
269+ # Both formats should produce the same result
270+ ce_from_dict = ComponentEvidence .from_json (json_data_dict ) # type: ignore[attr-defined]
271+ ce_from_array = ComponentEvidence .from_json (json_data_array ) # type: ignore[attr-defined]
272+
273+ self .assertEqual (len (ce_from_dict .identity ), 1 )
274+ self .assertEqual (len (ce_from_array .identity ), 1 )
275+
276+ # The identity objects should be equivalent
277+ identity_dict = list (ce_from_dict .identity )[0 ]
278+ identity_array = list (ce_from_array .identity )[0 ]
279+ self .assertEqual (identity_dict .field , identity_array .field )
280+ self .assertEqual (identity_dict .confidence , identity_array .confidence )
281+ self .assertEqual (identity_dict .concluded_value , identity_array .concluded_value )
282+
283+ def test_identity_dict_with_multiple_methods (self ) -> None :
284+ """Test deserialization of single identity dict with multiple methods"""
285+ json_data = {
286+ 'identity' : {
287+ 'field' : 'purl' ,
288+ 'confidence' : 0.95 ,
289+ 'concludedValue' : 'pkg:npm/example@1.0.0' ,
290+ 'methods' : [
291+ {
292+ 'technique' : 'source-code-analysis' ,
293+ 'confidence' : 0.9 ,
294+ 'value' : 'Found in package.json'
295+ },
296+ {
297+ 'technique' : 'binary-analysis' ,
298+ 'confidence' : 0.85 ,
299+ 'value' : 'Found in binary metadata'
300+ }
301+ ]
302+ }
303+ }
304+ ce = ComponentEvidence .from_json (json_data ) # type: ignore[attr-defined]
305+ self .assertEqual (len (ce .identity ), 1 )
306+ identity = list (ce .identity )[0 ]
307+ self .assertEqual (identity .field , IdentityField .PURL )
308+ self .assertEqual (len (identity .methods ), 2 )
309+
310+ # Verify methods are properly deserialized
311+ methods = sorted (identity .methods , key = lambda m : m .technique .value )
312+ self .assertEqual (methods [0 ].technique , AnalysisTechnique .BINARY_ANALYSIS )
313+ self .assertEqual (methods [0 ].confidence , Decimal ('0.85' ))
314+ self .assertEqual (methods [1 ].technique , AnalysisTechnique .SOURCE_CODE_ANALYSIS )
315+ self .assertEqual (methods [1 ].confidence , Decimal ('0.9' ))
316+
317+ def test_identity_deserialization_dict_with_methods (self ) -> None :
318+ """Test deserialization of single identity dict with methods"""
319+ json_data = {
320+ 'identity' : {
321+ 'field' : 'name' ,
322+ 'confidence' : 0.95 ,
323+ 'concludedValue' : 'test-lib' ,
324+ 'methods' : [
325+ {
326+ 'technique' : 'source-code-analysis' ,
327+ 'confidence' : 0.9 ,
328+ 'value' : 'Found in metadata'
329+ }
330+ ]
331+ }
332+ }
333+ ce = ComponentEvidence .from_json (json_data ) # type: ignore[attr-defined]
334+ self .assertEqual (len (ce .identity ), 1 )
335+ identity = list (ce .identity )[0 ]
336+ self .assertEqual (len (identity .methods ), 1 )
337+ method = list (identity .methods )[0 ]
338+ self .assertEqual (method .technique , AnalysisTechnique .SOURCE_CODE_ANALYSIS )
339+ self .assertEqual (method .confidence , Decimal ('0.9' ))
340+
204341
205342class TestModelCallStackFrame (TestCase ):
206343
0 commit comments