@@ -108,12 +108,22 @@ def setUp(self):
108108 self .client .credentials (HTTP_AUTHORIZATION = "Token " + token .key )
109109 self .client .force_login (self .get_test_admin ())
110110
111+ def _base_create_payload (self ):
112+ # Reuse finding id=2 as a template; drop the identity/derived CWE fields so each test
113+ # supplies exactly the cwe/cwes it is exercising.
114+ payload = self .get_finding_api (2 )
115+ del payload ["id" ]
116+ payload .pop ("cve" , None )
117+ payload .pop ("cwe" , None )
118+ payload .pop ("cwes" , None )
119+ return payload
120+
121+ def _stored_cwes (self , finding_id ):
122+ return set (Finding_CWE .objects .filter (finding_id = finding_id ).values_list ("cwe" , flat = True ))
123+
111124 def test_finding_create_with_cwes (self ):
112125 # cwes mirror vulnerability_ids: nested [{"cwe": "CWE-79"}], first is the primary Finding.cwe
113- finding_details = self .get_finding_api (2 )
114- del finding_details ["id" ]
115- finding_details .pop ("cve" , None )
116- finding_details .pop ("cwe" , None )
126+ finding_details = self ._base_create_payload ()
117127 new_cwes = [{"cwe" : "CWE-79" }, {"cwe" : "CWE-89" }]
118128 finding_details ["cwes" ] = new_cwes
119129 response = self .post_new_finding_api (finding_details )
@@ -124,6 +134,89 @@ def test_finding_create_with_cwes(self):
124134 self .assertEqual (79 , finding .cwe )
125135 self .assertEqual ({"CWE-79" , "CWE-89" }, set (finding .finding_cwe_set .values_list ("cwe" , flat = True )))
126136
137+ def test_finding_create_precedence_explicit_scalar_wins (self ):
138+ # When BOTH a scalar `cwe` and a `cwes` list are supplied, the explicit scalar stays the
139+ # primary; the cwes entries become the extras (mirrors vulnerability_ids precedence).
140+ finding_details = self ._base_create_payload ()
141+ finding_details ["cwe" ] = 22
142+ finding_details ["cwes" ] = [{"cwe" : "CWE-79" }, {"cwe" : "CWE-89" }]
143+ response = self .post_new_finding_api (finding_details )
144+ finding = Finding .objects .get (id = response .get ("id" ))
145+ self .assertEqual (22 , finding .cwe )
146+ self .assertEqual ({"CWE-22" , "CWE-79" , "CWE-89" }, self ._stored_cwes (finding .id ))
147+ # primary-first ordering guaranteed by the model property / save_cwes
148+ self .assertEqual ("CWE-22" , finding .cwes [0 ])
149+
150+ def test_finding_create_cwes_only_first_becomes_primary (self ):
151+ # No scalar cwe supplied -> the first cwes entry becomes the primary and is mirrored.
152+ finding_details = self ._base_create_payload ()
153+ finding_details ["cwes" ] = [{"cwe" : "CWE-89" }, {"cwe" : "CWE-22" }]
154+ response = self .post_new_finding_api (finding_details )
155+ finding = Finding .objects .get (id = response .get ("id" ))
156+ self .assertEqual (89 , finding .cwe )
157+ self .assertEqual ({"CWE-89" , "CWE-22" }, self ._stored_cwes (finding .id ))
158+ self .assertEqual ("CWE-89" , finding .cwes [0 ])
159+
160+ def test_finding_update_replaces_cwes (self ):
161+ finding_details = self ._base_create_payload ()
162+ finding_details ["cwes" ] = [{"cwe" : "CWE-79" }, {"cwe" : "CWE-89" }]
163+ finding_id = self .post_new_finding_api (finding_details ).get ("id" )
164+ # PATCH with a new set replaces the rows entirely (delete-then-recreate via save_cwes).
165+ self .patch_finding_api (finding_id , {"cwes" : [{"cwe" : "CWE-22" }, {"cwe" : "CWE-352" }]})
166+ finding = Finding .objects .get (id = finding_id )
167+ self .assertEqual (22 , finding .cwe )
168+ self .assertEqual ({"CWE-22" , "CWE-352" }, self ._stored_cwes (finding_id ))
169+
170+ def test_finding_update_precedence_explicit_scalar_wins (self ):
171+ finding_details = self ._base_create_payload ()
172+ finding_details ["cwes" ] = [{"cwe" : "CWE-79" }]
173+ finding_id = self .post_new_finding_api (finding_details ).get ("id" )
174+ # Both scalar + list on the same PATCH: explicit scalar stays primary.
175+ self .patch_finding_api (finding_id , {"cwe" : 22 , "cwes" : [{"cwe" : "CWE-79" }, {"cwe" : "CWE-89" }]})
176+ finding = Finding .objects .get (id = finding_id )
177+ self .assertEqual (22 , finding .cwe )
178+ self .assertEqual ({"CWE-22" , "CWE-79" , "CWE-89" }, self ._stored_cwes (finding_id ))
179+ self .assertEqual ("CWE-22" , finding .cwes [0 ])
180+
181+ def test_finding_update_omission_leaves_cwes_untouched (self ):
182+ finding_details = self ._base_create_payload ()
183+ finding_details ["cwes" ] = [{"cwe" : "CWE-79" }, {"cwe" : "CWE-89" }]
184+ finding_id = self .post_new_finding_api (finding_details ).get ("id" )
185+ # A PATCH that omits cwes must not touch the Finding_CWE rows.
186+ self .patch_finding_api (finding_id , {"title" : "cwes untouched by this patch" })
187+ self .assertEqual ({"CWE-79" , "CWE-89" }, self ._stored_cwes (finding_id ))
188+
189+ def test_finding_update_explicit_empty_clears (self ):
190+ finding_details = self ._base_create_payload ()
191+ finding_details ["cwes" ] = [{"cwe" : "CWE-79" }, {"cwe" : "CWE-89" }]
192+ finding_id = self .post_new_finding_api (finding_details ).get ("id" )
193+ # Explicit [] clears the rows; with no scalar supplied the primary resets to unset (0).
194+ self .patch_finding_api (finding_id , {"cwes" : []})
195+ finding = Finding .objects .get (id = finding_id )
196+ self .assertEqual (set (), self ._stored_cwes (finding_id ))
197+ self .assertEqual (0 , finding .cwe )
198+
199+ def test_finding_update_explicit_empty_with_scalar_keeps_primary (self ):
200+ finding_details = self ._base_create_payload ()
201+ finding_details ["cwes" ] = [{"cwe" : "CWE-79" }, {"cwe" : "CWE-89" }]
202+ finding_id = self .post_new_finding_api (finding_details ).get ("id" )
203+ # Explicit [] with an explicit scalar clears the extras but keeps the scalar-derived primary.
204+ self .patch_finding_api (finding_id , {"cwe" : 79 , "cwes" : []})
205+ finding = Finding .objects .get (id = finding_id )
206+ self .assertEqual (79 , finding .cwe )
207+ self .assertEqual ({"CWE-79" }, self ._stored_cwes (finding_id ))
208+
209+ def test_finding_cwes_read_back_shape_is_object_list (self ):
210+ # Wire-shape stability: cwes reads back as a list of {"cwe": "CWE-<n>"} objects, never a
211+ # flat list of ints. This is the contract v2 must preserve.
212+ finding_details = self ._base_create_payload ()
213+ finding_details ["cwes" ] = [{"cwe" : "CWE-79" }, {"cwe" : "CWE-89" }]
214+ finding_id = self .post_new_finding_api (finding_details ).get ("id" )
215+ cwes = self .get_finding_api (finding_id )["cwes" ]
216+ self .assertTrue (all (isinstance (entry , dict ) and set (entry .keys ()) == {"cwe" } for entry in cwes ))
217+ self .assertTrue (all (entry ["cwe" ].startswith ("CWE-" ) for entry in cwes ))
218+ self .assertEqual ({"CWE-79" , "CWE-89" }, {entry ["cwe" ] for entry in cwes })
219+
127220
128221@versioned_fixtures
129222class TestFindingCweHashCode (DojoTestCase ):
0 commit comments