@@ -15,6 +15,7 @@ jest.mock('@urbackend/common', () => {
1515 return {
1616 Webhook : {
1717 create : jest . fn ( ) ,
18+ countDocuments : jest . fn ( ) ,
1819 find : jest . fn ( ) ,
1920 findOne : jest . fn ( ) ,
2021 findOneAndUpdate : jest . fn ( ) ,
@@ -26,6 +27,7 @@ jest.mock('@urbackend/common', () => {
2627 } ,
2728 Project : {
2829 findOne : jest . fn ( ) ,
30+ updateOne : jest . fn ( ) ,
2931 } ,
3032 AppError,
3133 encrypt : jest . fn ( ( val ) => ( { encrypted : 'enc' , iv : 'iv' , tag : 'tag' } ) ) ,
@@ -95,6 +97,10 @@ describe('webhook.controller', () => {
9597 next = jest . fn ( ) ;
9698 } ) ;
9799
100+ afterEach ( ( ) => {
101+ jest . restoreAllMocks ( ) ;
102+ } ) ;
103+
98104 describe ( 'createWebhook' , ( ) => {
99105 test ( 'creates webhook with valid input' , async ( ) => {
100106 Project . findOne . mockResolvedValue ( { _id : validProjectId } ) ;
@@ -145,6 +151,90 @@ describe('webhook.controller', () => {
145151 ) ;
146152 } ) ;
147153
154+ test ( 'checks finite quota and creates the webhook in one transaction' , async ( ) => {
155+ const session = {
156+ withTransaction : jest . fn ( async ( operation ) => operation ( ) ) ,
157+ endSession : jest . fn ( ) ,
158+ } ;
159+ jest . spyOn ( mongoose , 'startSession' ) . mockResolvedValue ( session ) ;
160+ Project . findOne . mockResolvedValue ( { _id : validProjectId } ) ;
161+ Project . updateOne . mockResolvedValue ( { matchedCount : 1 } ) ;
162+ Webhook . countDocuments . mockResolvedValue ( 2 ) ;
163+ createWebhookSchema . safeParse . mockReturnValue ( {
164+ success : true ,
165+ data : {
166+ name : 'Test Webhook' ,
167+ url : 'https://example.com/hook' ,
168+ secret : 'whsec_testsecret12345678' ,
169+ events : { } ,
170+ } ,
171+ } ) ;
172+ Webhook . create . mockResolvedValue ( [ {
173+ _id : validWebhookId ,
174+ projectId : validProjectId ,
175+ name : 'Test Webhook' ,
176+ url : 'https://example.com/hook' ,
177+ events : new Map ( ) ,
178+ enabled : true ,
179+ } ] ) ;
180+ req . body = {
181+ name : 'Test Webhook' ,
182+ url : 'https://example.com/hook' ,
183+ secret : 'whsec_testsecret12345678' ,
184+ } ;
185+ req . webhookQuotaLimit = 3 ;
186+
187+ await createWebhook ( req , res , next ) ;
188+
189+ expect ( Project . updateOne ) . toHaveBeenCalledWith (
190+ { _id : validProjectId , owner : 'user123' } ,
191+ { $inc : { webhookQuotaVersion : 1 } } ,
192+ { session }
193+ ) ;
194+ expect ( Webhook . countDocuments ) . toHaveBeenCalledWith (
195+ { projectId : validProjectId } ,
196+ { session }
197+ ) ;
198+ expect ( Webhook . create ) . toHaveBeenCalledWith (
199+ [ expect . objectContaining ( { projectId : validProjectId , name : 'Test Webhook' } ) ] ,
200+ { session }
201+ ) ;
202+ expect ( session . endSession ) . toHaveBeenCalled ( ) ;
203+ expect ( res . status ) . toHaveBeenCalledWith ( 201 ) ;
204+ } ) ;
205+
206+ test ( 'does not create a webhook when the transactional quota reservation fails' , async ( ) => {
207+ const session = {
208+ withTransaction : jest . fn ( async ( operation ) => operation ( ) ) ,
209+ endSession : jest . fn ( ) ,
210+ } ;
211+ jest . spyOn ( mongoose , 'startSession' ) . mockResolvedValue ( session ) ;
212+ Project . findOne . mockResolvedValue ( { _id : validProjectId } ) ;
213+ Project . updateOne . mockResolvedValue ( { matchedCount : 1 } ) ;
214+ Webhook . countDocuments . mockResolvedValue ( 3 ) ;
215+ createWebhookSchema . safeParse . mockReturnValue ( {
216+ success : true ,
217+ data : {
218+ name : 'Test Webhook' ,
219+ url : 'https://example.com/hook' ,
220+ secret : 'whsec_testsecret12345678' ,
221+ } ,
222+ } ) ;
223+ req . body = {
224+ name : 'Test Webhook' ,
225+ url : 'https://example.com/hook' ,
226+ secret : 'whsec_testsecret12345678' ,
227+ } ;
228+ req . webhookQuotaLimit = 3 ;
229+
230+ await createWebhook ( req , res , next ) ;
231+
232+ expect ( Webhook . create ) . not . toHaveBeenCalled ( ) ;
233+ expect ( session . endSession ) . toHaveBeenCalled ( ) ;
234+ expect ( next ) . toHaveBeenCalledWith ( expect . any ( AppError ) ) ;
235+ expect ( next . mock . calls [ 0 ] [ 0 ] . statusCode ) . toBe ( 403 ) ;
236+ } ) ;
237+
148238 test ( 'returns 404 if project not found' , async ( ) => {
149239 Project . findOne . mockResolvedValue ( null ) ;
150240
0 commit comments