11import { EntityType } from '@dcl/schemas'
2- import { sleep } from '@dcl/snapshots-fetcher/dist/utils'
32import LeakDetector from 'jest-leak-detector'
43import { createDeployRateLimiter } from '../../../../src/ports/deployRateLimiterComponent'
54import { makeNoopValidator } from '../../../helpers/service/validations/NoOpValidator'
6- import { buildDeployData , buildDeployDataAfterEntity } from '../../E2ETestUtils'
5+ import { buildDeployData , buildDeployDataAfterEntity , EntityCombo } from '../../E2ETestUtils'
76import { TestProgram } from '../../TestProgram'
87import { createDefaultServer , resetServer } from '../../simpleTestEnvironment'
98
10- // Short TTLs for testing (in milliseconds — converted to seconds internally by the rate limiter )
9+ // Short TTLs for testing (in milliseconds)
1110const NORMAL_TTL_MS = 2000
1211const UNCHANGED_TTL_MS = 5000
1312
1413describe ( 'Rate limiting E2E' , ( ) => {
1514 let server : TestProgram
15+ let currentTime : number
16+ let dateNowSpy : jest . SpyInstance
17+
18+ /**
19+ * Advances the mocked Date.now() by the given milliseconds.
20+ * NodeCache and the deployer's Clock component both rely on Date.now(),
21+ * so this controls TTL expiration without real sleeps.
22+ */
23+ function advanceTime ( ms : number ) : void {
24+ currentTime += ms
25+ }
1626
1727 /**
1828 * Creates a real rate limiter with short TTLs and assigns its methods
1929 * onto the shared rate limiter object. This works because the deployer
2030 * captures a reference to the same object via Object.assign in the
2131 * test environment setup.
2232 */
23- function applyRealRateLimiter ( ) {
33+ function applyRealRateLimiter ( ) : void {
2434 const realRateLimiter = createDeployRateLimiter (
2535 { logs : server . components . logs } ,
2636 {
@@ -36,17 +46,20 @@ describe('Rate limiting E2E', () => {
3646
3747 beforeAll ( async ( ) => {
3848 server = await createDefaultServer ( )
39- // Bypass protocol validation (blockchain checks) but keep server
40- // validation active — rate limiting is part of server validation
4149 makeNoopValidator ( server . components )
4250 } )
4351
4452 beforeEach ( async ( ) => {
53+ currentTime = Date . now ( )
54+ dateNowSpy = jest . spyOn ( Date , 'now' ) . mockImplementation ( ( ) => currentTime )
4555 await resetServer ( server )
46- // Fresh rate limiter per test so cached entries don't leak across tests
4756 applyRealRateLimiter ( )
4857 } )
4958
59+ afterEach ( ( ) => {
60+ dateNowSpy . mockRestore ( )
61+ } )
62+
5063 afterAll ( async ( ) => {
5164 jest . restoreAllMocks ( )
5265 const detector = new LeakDetector ( server )
@@ -55,101 +68,125 @@ describe('Rate limiting E2E', () => {
5568 expect ( await detector . isLeaking ( ) ) . toBe ( false )
5669 } )
5770
58- it ( 'should reject a deployment to the same pointer within the rate limit TTL' , async ( ) => {
59- const { deployData : d1 } = await buildDeployData ( [ 'X100,Y100' , 'X100,Y101' ] , {
60- metadata : { v : 1 }
71+ describe ( 'when deploying to the same pointer within the normal TTL' , ( ) => {
72+ let firstDeploy : EntityCombo
73+ let secondDeploy : EntityCombo
74+
75+ beforeEach ( async ( ) => {
76+ firstDeploy = await buildDeployData ( [ 'X100,Y100' , 'X100,Y101' ] , {
77+ metadata : { v : 1 }
78+ } )
79+ await server . deployEntity ( firstDeploy . deployData )
80+
81+ secondDeploy = await buildDeployDataAfterEntity (
82+ { timestamp : currentTime } ,
83+ [ 'X100,Y100' , 'X100,Y101' ] ,
84+ { metadata : { v : 2 } }
85+ )
6186 } )
62- await server . deployEntity ( d1 )
6387
64- // Different entity (different metadata → different entity ID) to the same pointers
65- const { deployData : d2 } = await buildDeployDataAfterEntity (
66- { timestamp : Date . now ( ) } ,
67- [ 'X100,Y100' , 'X100,Y101' ] ,
68- { metadata : { v : 2 } }
69- )
70-
71- await expect ( server . deployEntity ( d2 ) ) . rejects . toThrow ( / r a t e l i m i t e d / i)
88+ it ( 'should reject the deployment' , async ( ) => {
89+ await expect ( server . deployEntity ( secondDeploy . deployData ) ) . rejects . toThrow ( / r a t e l i m i t e d / i)
90+ } )
7291 } )
7392
74- it ( 'should allow a deployment after the normal rate limit TTL expires' , async ( ) => {
75- const { deployData : d1 } = await buildDeployData ( [ 'X200,Y200' ] , {
76- metadata : { v : 1 }
77- } )
78- const ts1 = await server . deployEntity ( d1 )
93+ describe ( 'when deploying after the normal TTL has expired' , ( ) => {
94+ let firstTimestamp : number
95+ let secondDeploy : EntityCombo
7996
80- // Wait for the 2s TTL to expire
81- await sleep ( NORMAL_TTL_MS + 1000 )
97+ beforeEach ( async ( ) => {
98+ const { deployData : d1 } = await buildDeployData ( [ 'X200,Y200' ] , {
99+ metadata : { v : 1 }
100+ } )
101+ firstTimestamp = await server . deployEntity ( d1 )
82102
83- const { deployData : d2 } = await buildDeployDataAfterEntity (
84- { timestamp : ts1 } ,
85- [ 'X200,Y200' ] ,
86- { metadata : { v : 2 } }
87- )
103+ advanceTime ( NORMAL_TTL_MS + 1000 )
104+
105+ secondDeploy = await buildDeployDataAfterEntity (
106+ { timestamp : firstTimestamp } ,
107+ [ 'X200,Y200' ] ,
108+ { metadata : { v : 2 } }
109+ )
110+ } )
88111
89- // Should succeed — TTL has expired
90- const ts2 = await server . deployEntity ( d2 )
91- expect ( ts2 ) . toBeGreaterThan ( ts1 )
112+ it ( 'should allow the deployment' , async ( ) => {
113+ const secondTimestamp : number = await server . deployEntity ( secondDeploy . deployData )
114+ expect ( secondTimestamp ) . toBeGreaterThan ( firstTimestamp )
115+ } )
92116 } )
93117
94- it ( 'should apply a longer rate limit TTL when the same metadata is re-deployed' , async ( ) => {
118+ describe ( 'when re-deploying with the same metadata (unchanged content)' , ( ) => {
95119 const metadata = { outfit : 'red-shirt' , version : 1 }
120+ let secondTimestamp : number
96121
97- // Deploy 1: initial deployment
98- const { deployData : d1 } = await buildDeployData ( [ 'X300,Y300' ] , { metadata } )
99- const ts1 = await server . deployEntity ( d1 )
122+ beforeEach ( async ( ) => {
123+ const { deployData : d1 } = await buildDeployData ( [ 'X300,Y300' ] , { metadata } )
124+ const firstTimestamp : number = await server . deployEntity ( d1 )
100125
101- // Wait for normal TTL (2s) to expire
102- await sleep ( NORMAL_TTL_MS + 1000 )
126+ advanceTime ( NORMAL_TTL_MS + 1000 )
103127
104- // Deploy 2: same metadata → succeeds, but registers in the unchanged cache (5s TTL)
105- const { deployData : d2 } = await buildDeployDataAfterEntity (
106- { timestamp : ts1 } ,
107- [ 'X300,Y300' ] ,
108- { metadata }
109- )
110- const ts2 = await server . deployEntity ( d2 )
111- expect ( ts2 ) . toBeGreaterThan ( ts1 )
128+ const { deployData : d2 } = await buildDeployDataAfterEntity (
129+ { timestamp : firstTimestamp } ,
130+ [ 'X300,Y300' ] ,
131+ { metadata }
132+ )
133+ secondTimestamp = await server . deployEntity ( d2 )
134+ } )
112135
113- // Wait for normal TTL (2s) to expire, but NOT the unchanged TTL (5s)
114- await sleep ( NORMAL_TTL_MS + 1000 )
136+ describe ( 'and the normal TTL has expired but the unchanged TTL has not' , ( ) => {
137+ let thirdDeploy : EntityCombo
115138
116- // Deploy 3: same metadata again → should be rate-limited by the unchanged cache
117- const { deployData : d3 } = await buildDeployDataAfterEntity (
118- { timestamp : ts2 } ,
119- [ 'X300,Y300' ] ,
120- { metadata }
121- )
122- await expect ( server . deployEntity ( d3 ) ) . rejects . toThrow ( / r a t e l i m i t e d / i)
139+ beforeEach ( async ( ) => {
140+ advanceTime ( NORMAL_TTL_MS + 1000 )
141+
142+ thirdDeploy = await buildDeployDataAfterEntity (
143+ { timestamp : secondTimestamp } ,
144+ [ 'X300,Y300' ] ,
145+ { metadata }
146+ )
147+ } )
148+
149+ it ( 'should reject the deployment' , async ( ) => {
150+ await expect ( server . deployEntity ( thirdDeploy . deployData ) ) . rejects . toThrow ( / r a t e l i m i t e d / i)
151+ } )
152+ } )
123153 } )
124154
125- it ( 'should allow a deployment with changed metadata even when the unchanged rate limit TTL is active' , async ( ) => {
155+ describe ( ' when the unchanged TTL is active from a previous identical deployment' , ( ) => {
126156 const metadata = { outfit : 'red-shirt' , version : 1 }
157+ let secondTimestamp : number
127158
128- // Deploy 1: initial deployment
129- const { deployData : d1 } = await buildDeployData ( [ 'X400,Y400' ] , { metadata } )
130- const ts1 = await server . deployEntity ( d1 )
159+ beforeEach ( async ( ) => {
160+ const { deployData : d1 } = await buildDeployData ( [ 'X400,Y400' ] , { metadata } )
161+ const firstTimestamp : number = await server . deployEntity ( d1 )
131162
132- // Wait for normal TTL to expire
133- await sleep ( NORMAL_TTL_MS + 1000 )
163+ advanceTime ( NORMAL_TTL_MS + 1000 )
134164
135- // Deploy 2: same metadata → sets the unchanged cache (5s TTL)
136- const { deployData : d2 } = await buildDeployDataAfterEntity (
137- { timestamp : ts1 } ,
138- [ 'X400,Y400' ] ,
139- { metadata }
140- )
141- const ts2 = await server . deployEntity ( d2 )
165+ const { deployData : d2 } = await buildDeployDataAfterEntity (
166+ { timestamp : firstTimestamp } ,
167+ [ 'X400,Y400' ] ,
168+ { metadata }
169+ )
170+ secondTimestamp = await server . deployEntity ( d2 )
142171
143- // Wait for normal TTL to expire, but NOT unchanged TTL
144- await sleep ( NORMAL_TTL_MS + 1000 )
172+ advanceTime ( NORMAL_TTL_MS + 1000 )
173+ } )
145174
146- // Deploy 3: DIFFERENT metadata → unchanged check doesn't apply → should succeed
147- const { deployData : d3 } = await buildDeployDataAfterEntity (
148- { timestamp : ts2 } ,
149- [ 'X400,Y400' ] ,
150- { metadata : { outfit : 'blue-shirt' , version : 2 } }
151- )
152- const ts3 = await server . deployEntity ( d3 )
153- expect ( ts3 ) . toBeGreaterThan ( ts2 )
175+ describe ( 'and the new deployment has different metadata' , ( ) => {
176+ let thirdDeploy : EntityCombo
177+
178+ beforeEach ( async ( ) => {
179+ thirdDeploy = await buildDeployDataAfterEntity (
180+ { timestamp : secondTimestamp } ,
181+ [ 'X400,Y400' ] ,
182+ { metadata : { outfit : 'blue-shirt' , version : 2 } }
183+ )
184+ } )
185+
186+ it ( 'should allow the deployment' , async ( ) => {
187+ const thirdTimestamp : number = await server . deployEntity ( thirdDeploy . deployData )
188+ expect ( thirdTimestamp ) . toBeGreaterThan ( secondTimestamp )
189+ } )
190+ } )
154191 } )
155192} )
0 commit comments