@@ -21,20 +21,12 @@ vi.mock("nodemailer", () => ({
2121 default : { createTransport : ( ) => ( { sendMail } ) } ,
2222} ) ) ;
2323
24- import { makeLimiter } from "../middleware/rate-limit" ;
2524import { contactRouter } from "./contact" ;
2625
2726function makeApp ( ) {
2827 const app = express ( ) ;
2928 app . set ( "trust proxy" , 1 ) ;
3029 app . use ( express . json ( ) ) ;
31- const contactLimiter = makeLimiter ( {
32- name : "contact" ,
33- windowMs : envMock . RATE_LIMIT_CONTACT_WINDOW_MS ,
34- max : envMock . RATE_LIMIT_CONTACT_MAX ,
35- message : "Too many contact form submissions. Please try again later." ,
36- } ) ;
37- app . use ( "/api/v1/contact" , contactLimiter ) ;
3830 app . use ( "/api/v1" , contactRouter ) ;
3931 return app ;
4032}
@@ -58,6 +50,7 @@ describe("POST /contact/send-message", () => {
5850 it ( "rejects missing fields with 400 and does not send" , async ( ) => {
5951 const res = await request ( makeApp ( ) )
6052 . post ( "/api/v1/contact/send-message" )
53+ . set ( "X-Forwarded-For" , "10.0.0.1" )
6154 . send ( { firstName : "Ada" } ) ;
6255 expect ( res . status ) . toBe ( 400 ) ;
6356 expect ( sendMail ) . not . toHaveBeenCalled ( ) ;
@@ -66,6 +59,7 @@ describe("POST /contact/send-message", () => {
6659 it ( "rejects an oversized message with 400" , async ( ) => {
6760 const res = await request ( makeApp ( ) )
6861 . post ( "/api/v1/contact/send-message" )
62+ . set ( "X-Forwarded-For" , "10.0.0.2" )
6963 . send ( { ...valid , message : "x" . repeat ( 5001 ) } ) ;
7064 expect ( res . status ) . toBe ( 400 ) ;
7165 expect ( sendMail ) . not . toHaveBeenCalled ( ) ;
@@ -74,12 +68,16 @@ describe("POST /contact/send-message", () => {
7468 it ( "rejects an invalid email with 400" , async ( ) => {
7569 const res = await request ( makeApp ( ) )
7670 . post ( "/api/v1/contact/send-message" )
71+ . set ( "X-Forwarded-For" , "10.0.0.3" )
7772 . send ( { ...valid , email : "not-an-email" } ) ;
7873 expect ( res . status ) . toBe ( 400 ) ;
7974 } ) ;
8075
8176 it ( "dev-mode without credentials returns success without sending" , async ( ) => {
82- const res = await request ( makeApp ( ) ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) ;
77+ const res = await request ( makeApp ( ) )
78+ . post ( "/api/v1/contact/send-message" )
79+ . set ( "X-Forwarded-For" , "10.0.0.4" )
80+ . send ( valid ) ;
8381 expect ( res . status ) . toBe ( 200 ) ;
8482 expect ( res . body . success ) . toBe ( true ) ;
8583 expect ( sendMail ) . not . toHaveBeenCalled ( ) ;
@@ -90,7 +88,10 @@ describe("POST /contact/send-message", () => {
9088 envMock . EMAIL_USER = "sender@gmail.com" ;
9189 envMock . EMAIL_PASSWORD = "app-password" ;
9290 // CONTACT_RECIPIENT_EMAIL intentionally left unset
93- const res = await request ( makeApp ( ) ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) ;
91+ const res = await request ( makeApp ( ) )
92+ . post ( "/api/v1/contact/send-message" )
93+ . set ( "X-Forwarded-For" , "10.0.0.5" )
94+ . send ( valid ) ;
9495 expect ( res . status ) . toBe ( 503 ) ;
9596 expect ( sendMail ) . not . toHaveBeenCalled ( ) ;
9697 } ) ;
@@ -101,6 +102,7 @@ describe("POST /contact/send-message", () => {
101102 envMock . CONTACT_RECIPIENT_EMAIL = "team@stellopay.com" ;
102103 const res = await request ( makeApp ( ) )
103104 . post ( "/api/v1/contact/send-message" )
105+ . set ( "X-Forwarded-For" , "10.0.0.6" )
104106 . send ( { ...valid , message : "<script>alert(1)</script>" } ) ;
105107
106108 expect ( res . status ) . toBe ( 200 ) ;
@@ -116,7 +118,10 @@ describe("POST /contact/send-message", () => {
116118 envMock . EMAIL_PASSWORD = "app-password" ;
117119 envMock . CONTACT_RECIPIENT_EMAIL = "team@stellopay.com" ;
118120 sendMail . mockRejectedValueOnce ( new Error ( "smtp down" ) ) ;
119- const res = await request ( makeApp ( ) ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) ;
121+ const res = await request ( makeApp ( ) )
122+ . post ( "/api/v1/contact/send-message" )
123+ . set ( "X-Forwarded-For" , "10.0.0.7" )
124+ . send ( valid ) ;
120125 expect ( res . status ) . toBe ( 500 ) ;
121126 expect ( res . body . error ) . toMatch ( / F a i l e d t o s e n d / ) ;
122127 } ) ;
@@ -127,7 +132,10 @@ describe("POST /contact/send-message", () => {
127132 envMock . EMAIL_PASSWORD = "app-password" ;
128133 envMock . CONTACT_RECIPIENT_EMAIL = "team@stellopay.com" ;
129134 sendMail . mockRejectedValueOnce ( new Error ( "smtp down" ) ) ;
130- const res = await request ( makeApp ( ) ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) ;
135+ const res = await request ( makeApp ( ) )
136+ . post ( "/api/v1/contact/send-message" )
137+ . set ( "X-Forwarded-For" , "10.0.0.8" )
138+ . send ( valid ) ;
131139 expect ( res . status ) . toBe ( 500 ) ;
132140 expect ( res . body . details ) . toBeUndefined ( ) ;
133141 } ) ;
@@ -137,33 +145,54 @@ describe("POST /contact/send-message", () => {
137145 envMock . EMAIL_PASSWORD = "app-password" ;
138146 envMock . CONTACT_RECIPIENT_EMAIL = "team@stellopay.com" ;
139147 sendMail . mockRejectedValueOnce ( "smtp exploded" ) ; // a thrown string, not an Error
140- const res = await request ( makeApp ( ) ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) ;
148+ const res = await request ( makeApp ( ) )
149+ . post ( "/api/v1/contact/send-message" )
150+ . set ( "X-Forwarded-For" , "10.0.0.9" )
151+ . send ( valid ) ;
141152 expect ( res . status ) . toBe ( 500 ) ;
142153 } ) ;
143154} ) ;
144155
145156describe ( "Contact endpoint rate limiting" , ( ) => {
146157 it ( "allows requests up to the configured limit" , async ( ) => {
147- envMock . RATE_LIMIT_CONTACT_MAX = 3 ;
148158 const app = makeApp ( ) ;
149159
150- // First 3 requests should succeed
160+ // First 3 requests should succeed (default max). Use a unique IP
161+ // to avoid cross-test contamination from the module-scoped limiter.
151162 for ( let i = 0 ; i < 3 ; i ++ ) {
152- const res = await request ( app ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) ;
163+ const res = await request ( app )
164+ . post ( "/api/v1/contact/send-message" )
165+ . set ( "X-Forwarded-For" , "10.0.1.1" )
166+ . send ( valid ) ;
153167 expect ( res . status ) . toBe ( 200 ) ;
154168 }
155169 } ) ;
156170
157171 it ( "returns 429 with standard error envelope when limit is exceeded" , async ( ) => {
158- envMock . RATE_LIMIT_CONTACT_MAX = 2 ;
159172 const app = makeApp ( ) ;
160173
161- // First 2 requests succeed
162- await request ( app ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) . expect ( 200 ) ;
163- await request ( app ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) . expect ( 200 ) ;
174+ // First 3 requests succeed (default max) from a unique IP
175+ await request ( app )
176+ . post ( "/api/v1/contact/send-message" )
177+ . set ( "X-Forwarded-For" , "10.0.1.2" )
178+ . send ( valid )
179+ . expect ( 200 ) ;
180+ await request ( app )
181+ . post ( "/api/v1/contact/send-message" )
182+ . set ( "X-Forwarded-For" , "10.0.1.2" )
183+ . send ( valid )
184+ . expect ( 200 ) ;
185+ await request ( app )
186+ . post ( "/api/v1/contact/send-message" )
187+ . set ( "X-Forwarded-For" , "10.0.1.2" )
188+ . send ( valid )
189+ . expect ( 200 ) ;
164190
165- // Third request exceeds limit
166- const res = await request ( app ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) ;
191+ // Fourth request exceeds the default limit (max=3)
192+ const res = await request ( app )
193+ . post ( "/api/v1/contact/send-message" )
194+ . set ( "X-Forwarded-For" , "10.0.1.2" )
195+ . send ( valid ) ;
167196 expect ( res . status ) . toBe ( 429 ) ;
168197 expect ( res . body ) . toEqual ( {
169198 error : "Too many contact form submissions. Please try again later." ,
@@ -172,10 +201,19 @@ describe("Contact endpoint rate limiting", () => {
172201 } ) ;
173202
174203 it ( "enforces rate limiting per IP address" , async ( ) => {
175- envMock . RATE_LIMIT_CONTACT_MAX = 1 ;
176204 const app = makeApp ( ) ;
177205
178- // Client A exhausts their single request
206+ // Client A exhausts their 3 requests
207+ await request ( app )
208+ . post ( "/api/v1/contact/send-message" )
209+ . set ( "X-Forwarded-For" , "198.51.100.1" )
210+ . send ( valid )
211+ . expect ( 200 ) ;
212+ await request ( app )
213+ . post ( "/api/v1/contact/send-message" )
214+ . set ( "X-Forwarded-For" , "198.51.100.1" )
215+ . send ( valid )
216+ . expect ( 200 ) ;
179217 await request ( app )
180218 . post ( "/api/v1/contact/send-message" )
181219 . set ( "X-Forwarded-For" , "198.51.100.1" )
@@ -197,11 +235,14 @@ describe("Contact endpoint rate limiting", () => {
197235 } ) ;
198236
199237 it ( "preserves existing contact endpoint behaviour for valid requests" , async ( ) => {
200- envMock . RATE_LIMIT_CONTACT_MAX = 10 ;
201238 const app = makeApp ( ) ;
202239
203- // Valid request should still succeed with rate limiter applied
204- const res = await request ( app ) . post ( "/api/v1/contact/send-message" ) . send ( valid ) ;
240+ // Valid request should still succeed with rate limiter applied.
241+ // Use a unique IP to avoid cross-test contamination.
242+ const res = await request ( app )
243+ . post ( "/api/v1/contact/send-message" )
244+ . set ( "X-Forwarded-For" , "10.0.1.3" )
245+ . send ( valid ) ;
205246 expect ( res . status ) . toBe ( 200 ) ;
206247 expect ( res . body . success ) . toBe ( true ) ;
207248 } ) ;
0 commit comments