@@ -179,6 +179,84 @@ describe('Rulesets', () => {
179179 } )
180180 } )
181181
182+ describe ( 'idempotent create when the ruleset already exists (retried/concurrent POST)' , ( ) => {
183+ function duplicateNameError ( ) {
184+ const e = new Error ( 'Validation Failed' )
185+ e . status = 422
186+ e . response = { data : { errors : [ 'Name must be unique' ] } }
187+ return e
188+ }
189+
190+ function wireRequest ( routeResults ) {
191+ const calls = [ ]
192+ const request = jest . fn ( ) . mockImplementation ( ( route , body ) => {
193+ calls . push ( { route, body } )
194+ const handler = routeResults [ route ]
195+ return handler ? handler ( ) : Promise . resolve ( { url : route } )
196+ } )
197+ request . endpoint = jest . fn ( ) . mockImplementation ( ( route , body ) => ( { url : route , body } ) )
198+ request . endpoint . merge = jest . fn ( ) . mockImplementation ( ( route , body ) => ( { method : 'GET' , url : route , ...body } ) )
199+ github . request = request
200+ return calls
201+ }
202+
203+ it ( 'reconciles a repo ruleset by updating the existing one on 422 "Name must be unique"' , async ( ) => {
204+ const attrs = generateRequestRuleset ( 0 , 'synk' , repo_conditions , [ ] )
205+ delete attrs . id
206+ const existing = generateResponseRuleset ( 42 , 'synk' , repo_conditions , [ ] )
207+ const calls = wireRequest ( {
208+ 'POST /repos/{owner}/{repo}/rulesets' : ( ) => Promise . reject ( duplicateNameError ( ) )
209+ } )
210+ github . paginate = jest . fn ( )
211+ . mockResolvedValueOnce ( [ { id : 42 , name : 'synk' , source_type : 'Repository' } ] )
212+ . mockResolvedValueOnce ( [ existing ] )
213+
214+ const plugin = configure ( [ attrs ] )
215+ await plugin . add ( attrs )
216+
217+ const put = calls . find ( c => c . route === 'PUT /repos/{owner}/{repo}/rulesets/{id}' )
218+ expect ( put ) . toBeDefined ( )
219+ expect ( put . body . id ) . toBe ( 42 )
220+ } )
221+
222+ it ( 'reconciles an org ruleset by updating the existing one on 422 "Name must be unique"' , async ( ) => {
223+ const attrs = generateRequestRuleset ( 0 , 'synk' , org_conditions , [ ] , true )
224+ delete attrs . id
225+ const existing = generateResponseRuleset ( 7 , 'synk' , org_conditions , [ ] , true )
226+ const calls = wireRequest ( {
227+ 'POST /orgs/{org}/rulesets' : ( ) => Promise . reject ( duplicateNameError ( ) )
228+ } )
229+ github . paginate = jest . fn ( )
230+ . mockResolvedValueOnce ( [ { id : 7 , name : 'synk' , source_type : 'Organization' } ] )
231+ . mockResolvedValueOnce ( [ existing ] )
232+
233+ const plugin = configure ( [ attrs ] , 'org' )
234+ await plugin . add ( attrs )
235+
236+ const put = calls . find ( c => c . route === 'PUT /orgs/{org}/rulesets/{id}' )
237+ expect ( put ) . toBeDefined ( )
238+ expect ( put . body . id ) . toBe ( 7 )
239+ } )
240+
241+ it ( 'does not reconcile (surfaces the error) for a 422 that is not a name-uniqueness violation' , async ( ) => {
242+ const attrs = generateRequestRuleset ( 0 , 'synk' , repo_conditions , [ ] )
243+ delete attrs . id
244+ const other = new Error ( 'Validation Failed' )
245+ other . status = 422
246+ other . response = { data : { errors : [ 'Something else is invalid' ] } }
247+ const calls = wireRequest ( {
248+ 'POST /repos/{owner}/{repo}/rulesets' : ( ) => Promise . reject ( other )
249+ } )
250+ github . paginate = jest . fn ( )
251+
252+ const plugin = configure ( [ attrs ] )
253+ await plugin . add ( attrs )
254+
255+ expect ( github . paginate ) . not . toHaveBeenCalled ( )
256+ expect ( calls . some ( c => c . route === 'PUT /repos/{owner}/{repo}/rulesets/{id}' ) ) . toBe ( false )
257+ } )
258+ } )
259+
182260 describe ( 'when {{EXTERNALLY_DEFINED}} is present in "required_status_checks" and no status checks exist in GitHub' , ( ) => {
183261 it ( 'it initialises the status checks with an empty list' , ( ) => {
184262 // Mock the GitHub API response
0 commit comments