@@ -7,12 +7,10 @@ const { jest } = import.meta;
77const { mockEsmWithActual } = createMockUtils ( jest ) ;
88
99const mockGetTenantSubscription = jest . fn ( ) ;
10- const mockGetTenantUsageData = jest . fn ( ) ;
1110const mockReportSubscriptionUpdates = jest . fn ( ) ;
1211
1312await mockEsmWithActual ( '#src/utils/subscription/index.js' , ( ) => ( {
1413 getTenantSubscription : mockGetTenantSubscription ,
15- getTenantUsageData : mockGetTenantUsageData ,
1614 reportSubscriptionUpdates : mockReportSubscriptionUpdates ,
1715} ) ) ;
1816
@@ -22,6 +20,7 @@ const { QuotaLibrary } = await import('./quota.js');
2220
2321const originalIsCloud = EnvSet . values . isCloud ;
2422const originalIsIntegrationTest = EnvSet . values . isIntegrationTest ;
23+ const originalIsDevFeaturesEnabled = EnvSet . values . isDevFeaturesEnabled ;
2524
2625/**
2726 * These tests spin up a full MockTenant, which instantiates the real EnvSet and runs its load sequence.
@@ -30,7 +29,10 @@ const originalIsIntegrationTest = EnvSet.values.isIntegrationTest;
3029 * reference to the original EnvSet singleton. Mutating the live flags keeps both MockTenant and
3130 * QuotaLibrary aligned with the test scenarios without breaking their dependency chain.
3231 */
33- const setEnvFlag = ( key : 'isCloud' | 'isIntegrationTest' , value : boolean ) => {
32+ const setEnvFlag = (
33+ key : 'isCloud' | 'isIntegrationTest' | 'isDevFeaturesEnabled' ,
34+ value : boolean
35+ ) => {
3436 Reflect . set ( EnvSet . values , key , value ) ;
3537} ;
3638
@@ -57,22 +59,15 @@ beforeEach(() => {
5759 jest . clearAllMocks ( ) ;
5860 setEnvFlag ( 'isCloud' , true ) ;
5961 setEnvFlag ( 'isIntegrationTest' , false ) ;
62+ setEnvFlag ( 'isDevFeaturesEnabled' , originalIsDevFeaturesEnabled ) ;
6063
6164 mockGetTenantSubscription . mockResolvedValue ( mockSubscriptionData ) ;
62- mockGetTenantUsageData . mockResolvedValue ( {
63- quota : mockSubscriptionData . quota ,
64- usage : {
65- tenantMembersLimit : 0 ,
66- socialConnectorsLimit : 0 ,
67- } ,
68- resources : { } ,
69- roles : { } ,
70- } ) ;
7165} ) ;
7266
7367afterEach ( ( ) => {
7468 setEnvFlag ( 'isCloud' , originalIsCloud ) ;
7569 setEnvFlag ( 'isIntegrationTest' , originalIsIntegrationTest ) ;
70+ setEnvFlag ( 'isDevFeaturesEnabled' , originalIsDevFeaturesEnabled ) ;
7671} ) ;
7772
7873describe ( 'guardTenantUsageByKey' , ( ) => {
@@ -92,13 +87,18 @@ describe('guardTenantUsageByKey', () => {
9287 expect ( getSelfComputedUsageByKey ) . not . toHaveBeenCalled ( ) ;
9388 } ) ;
9489
95- it ( 'skips guard when quota limit is null' , async ( ) => {
90+ it ( 'skips guard when both quota and system limits are null/undefined' , async ( ) => {
91+ setEnvFlag ( 'isDevFeaturesEnabled' , true ) ;
9692 mockGetTenantSubscription . mockResolvedValueOnce ( {
9793 ...mockSubscriptionData ,
9894 quota : {
9995 ...mockSubscriptionData . quota ,
10096 applicationsLimit : null ,
10197 } ,
98+ systemLimit : {
99+ ...mockSubscriptionData . systemLimit ,
100+ applicationsLimit : undefined ,
101+ } ,
102102 } ) ;
103103
104104 const getSelfComputedUsageByKey = jest . fn ( ) ;
@@ -113,6 +113,62 @@ describe('guardTenantUsageByKey', () => {
113113 expect ( getSelfComputedUsageByKey ) . not . toHaveBeenCalled ( ) ;
114114 } ) ;
115115
116+ // Todo @xiaoyijun : remove once the dev feature guard is retired.
117+ it ( 'skips system limit guard when dev features are disabled' , async ( ) => {
118+ setEnvFlag ( 'isDevFeaturesEnabled' , false ) ;
119+ mockGetTenantSubscription . mockResolvedValueOnce ( {
120+ ...mockSubscriptionData ,
121+ quota : {
122+ ...mockSubscriptionData . quota ,
123+ applicationsLimit : null ,
124+ } ,
125+ systemLimit : {
126+ ...mockSubscriptionData . systemLimit ,
127+ applicationsLimit : 2 ,
128+ } ,
129+ } ) ;
130+
131+ const getSelfComputedUsageByKey = jest . fn ( ) . mockResolvedValue ( 2 ) ;
132+ const { quotaLibrary } = createQuotaLibrary ( {
133+ queriesOverride : {
134+ tenantUsage : { getSelfComputedUsageByKey } ,
135+ } ,
136+ } ) ;
137+
138+ await expect ( quotaLibrary . guardTenantUsageByKey ( 'applicationsLimit' ) ) . resolves . not . toThrow ( ) ;
139+
140+ expect ( getSelfComputedUsageByKey ) . not . toHaveBeenCalled ( ) ;
141+ } ) ;
142+
143+ it ( 'throws when usage reaches system limit' , async ( ) => {
144+ setEnvFlag ( 'isDevFeaturesEnabled' , true ) ;
145+ mockGetTenantSubscription . mockResolvedValueOnce ( {
146+ ...mockSubscriptionData ,
147+ quota : {
148+ ...mockSubscriptionData . quota ,
149+ applicationsLimit : null ,
150+ } ,
151+ systemLimit : {
152+ ...mockSubscriptionData . systemLimit ,
153+ applicationsLimit : 2 ,
154+ } ,
155+ } ) ;
156+
157+ const getSelfComputedUsageByKey = jest . fn ( ) . mockResolvedValue ( 2 ) ;
158+ const { quotaLibrary } = createQuotaLibrary ( {
159+ queriesOverride : {
160+ tenantUsage : { getSelfComputedUsageByKey } ,
161+ } ,
162+ } ) ;
163+
164+ await expect ( quotaLibrary . guardTenantUsageByKey ( 'applicationsLimit' ) ) . rejects . toMatchObject ( {
165+ code : 'system_limit.limit_exceeded' ,
166+ status : 403 ,
167+ } ) ;
168+
169+ expect ( getSelfComputedUsageByKey ) . toHaveBeenCalledTimes ( 1 ) ;
170+ } ) ;
171+
116172 it ( 'throws when boolean quota limit is disabled' , async ( ) => {
117173 const { quotaLibrary } = createQuotaLibrary ( ) ;
118174
@@ -171,12 +227,17 @@ describe('guardTenantUsageByKey', () => {
171227 } ) ;
172228
173229 it ( 'throws when numeric quota limit is not number type' , async ( ) => {
230+ setEnvFlag ( 'isDevFeaturesEnabled' , true ) ;
174231 mockGetTenantSubscription . mockResolvedValueOnce ( {
175232 ...mockSubscriptionData ,
176233 quota : {
177234 ...mockSubscriptionData . quota ,
178235 applicationsLimit : '3' as unknown as number ,
179236 } ,
237+ systemLimit : {
238+ ...mockSubscriptionData . systemLimit ,
239+ applicationsLimit : undefined ,
240+ } ,
180241 } ) ;
181242
182243 const getSelfComputedUsageByKey = jest . fn ( ) ;
@@ -209,35 +270,6 @@ describe('guardTenantUsageByKey', () => {
209270 } ) ;
210271 } ) ;
211272
212- it ( 'uses cloud usage data for tenantMembersLimit' , async ( ) => {
213- mockGetTenantSubscription . mockResolvedValueOnce ( {
214- ...mockSubscriptionData ,
215- quota : {
216- ...mockSubscriptionData . quota ,
217- tenantMembersLimit : 10 ,
218- } ,
219- } ) ;
220- mockGetTenantUsageData . mockResolvedValueOnce ( {
221- quota : mockSubscriptionData . quota ,
222- usage : { tenantMembersLimit : 5 } ,
223- resources : { } ,
224- roles : { } ,
225- } ) ;
226-
227- const getSelfComputedUsageByKey = jest . fn ( ) ;
228-
229- const { quotaLibrary, tenant } = createQuotaLibrary ( {
230- queriesOverride : {
231- tenantUsage : { getSelfComputedUsageByKey } ,
232- } ,
233- } ) ;
234-
235- await quotaLibrary . guardTenantUsageByKey ( 'tenantMembersLimit' ) ;
236-
237- expect ( mockGetTenantUsageData ) . toHaveBeenCalledWith ( tenant . cloudConnection ) ;
238- expect ( getSelfComputedUsageByKey ) . not . toHaveBeenCalled ( ) ;
239- } ) ;
240-
241273 it ( 'uses connector library for socialConnectorsLimit' , async ( ) => {
242274 mockGetTenantSubscription . mockResolvedValueOnce ( {
243275 ...mockSubscriptionData ,
@@ -281,13 +313,13 @@ describe('guardTenantUsageByKey', () => {
281313 } ,
282314 } ) ;
283315
284- await quotaLibrary . guardTenantUsageByKey ( 'scopesPerResourceLimit' , {
285- entityId : 'resource_1' ,
286- } ) ;
316+ await quotaLibrary . guardTenantUsageByKey ( 'scopesPerResourceLimit' , 'resource_1' ) ;
287317
288- expect ( getSelfComputedUsageByKey ) . toHaveBeenCalledWith ( tenant . id , 'scopesPerResourceLimit' , {
289- entityId : 'resource_1' ,
290- } ) ;
318+ expect ( getSelfComputedUsageByKey ) . toHaveBeenCalledWith (
319+ tenant . id ,
320+ 'scopesPerResourceLimit' ,
321+ 'resource_1'
322+ ) ;
291323 } ) ;
292324
293325 it ( 'throws when entity usage exceeds limit' , async ( ) => {
@@ -308,24 +340,25 @@ describe('guardTenantUsageByKey', () => {
308340 } ) ;
309341
310342 await expect (
311- quotaLibrary . guardTenantUsageByKey ( 'scopesPerRoleLimit' , { entityId : 'role_1' } )
343+ quotaLibrary . guardTenantUsageByKey ( 'scopesPerRoleLimit' , 'role_1' )
312344 ) . rejects . toMatchObject ( {
313345 code : 'subscription.limit_exceeded' ,
314346 status : 403 ,
315347 } ) ;
316348 } ) ;
317349
318350 it ( 'skips guard for add-on usage keys on paid plans' , async ( ) => {
351+ setEnvFlag ( 'isDevFeaturesEnabled' , false ) ;
319352 mockGetTenantSubscription . mockResolvedValueOnce ( {
320353 ...mockSubscriptionData ,
321- planId : ReservedPlanId . Pro ,
354+ planId : ReservedPlanId . Pro202509 ,
322355 quota : {
323356 ...mockSubscriptionData . quota ,
324357 machineToMachineLimit : 1 ,
325358 } ,
326359 } ) ;
327360
328- const getSelfComputedUsageByKey = jest . fn ( ) ;
361+ const getSelfComputedUsageByKey = jest . fn ( ) . mockResolvedValue ( 0 ) ;
329362
330363 const { quotaLibrary } = createQuotaLibrary ( {
331364 queriesOverride : {
@@ -337,6 +370,35 @@ describe('guardTenantUsageByKey', () => {
337370
338371 expect ( getSelfComputedUsageByKey ) . not . toHaveBeenCalled ( ) ;
339372 } ) ;
373+
374+ it ( 'calls getTenantUsageByKey only once when both system limit and quota limit checks are needed' , async ( ) => {
375+ setEnvFlag ( 'isDevFeaturesEnabled' , true ) ;
376+ mockGetTenantSubscription . mockResolvedValueOnce ( {
377+ ...mockSubscriptionData ,
378+ quota : {
379+ ...mockSubscriptionData . quota ,
380+ applicationsLimit : 10 , // Quota limit set
381+ } ,
382+ systemLimit : {
383+ ...mockSubscriptionData . systemLimit ,
384+ applicationsLimit : 5 , // System limit set (lower than quota)
385+ } ,
386+ } ) ;
387+
388+ const getSelfComputedUsageByKey = jest . fn ( ) . mockResolvedValue ( 2 ) ;
389+
390+ const { quotaLibrary } = createQuotaLibrary ( {
391+ queriesOverride : {
392+ tenantUsage : { getSelfComputedUsageByKey } ,
393+ } ,
394+ } ) ;
395+
396+ // Should pass both checks: 2 < 5 (system limit) and 2 < 10 (quota limit)
397+ await expect ( quotaLibrary . guardTenantUsageByKey ( 'applicationsLimit' ) ) . resolves . not . toThrow ( ) ;
398+
399+ // The key point: usage should be fetched only once, not twice
400+ expect ( getSelfComputedUsageByKey ) . toHaveBeenCalledTimes ( 1 ) ;
401+ } ) ;
340402} ) ;
341403
342404describe ( 'reportSubscriptionUpdatesUsage' , ( ) => {
0 commit comments