@@ -21,7 +21,7 @@ describe("QuotePage", () => {
2121 } ) ;
2222
2323 it ( "calls the backend and renders the route on success" , async ( ) => {
24- globalThis . fetch = jest . fn ( ) . mockResolvedValueOnce ( {
24+ const mockFetch = jest . fn ( ) . mockResolvedValueOnce ( {
2525 ok : true ,
2626 json : async ( ) => ( {
2727 source_asset : "USDC" ,
@@ -31,6 +31,7 @@ describe("QuotePage", () => {
3131 route : [ "USDC" , "EURC" ] ,
3232 } ) ,
3333 } as unknown as Response ) ;
34+ globalThis . fetch = mockFetch as unknown as typeof globalThis . fetch ;
3435
3536 render ( < QuotePage /> ) ;
3637 fireEvent . change ( screen . getByLabelText ( / S o u r c e a s s e t / i) , {
@@ -47,6 +48,9 @@ describe("QuotePage", () => {
4748 await waitFor ( ( ) => {
4849 expect ( screen . getByRole ( "status" ) ) . toHaveTextContent ( / U S D C → E U R C / ) ;
4950 } ) ;
51+ expect ( mockFetch ) . toHaveBeenCalledWith (
52+ expect . stringContaining ( "source_asset=USDC&dest_asset=EURC&amount=1000000" ) ,
53+ ) ;
5054 } ) ;
5155
5256 it ( "blocks submission when source == destination" , async ( ) => {
@@ -95,6 +99,105 @@ describe("QuotePage", () => {
9599 expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
96100 } ) ;
97101
102+ it ( "blocks submission when an asset code contains unsafe characters" , async ( ) => {
103+ const mockFetch = jest . fn ( ) ;
104+ globalThis . fetch = mockFetch as unknown as typeof globalThis . fetch ;
105+ render ( < QuotePage /> ) ;
106+
107+ fireEvent . change ( screen . getByLabelText ( / S o u r c e a s s e t / i) , {
108+ target : { value : "US$C" } ,
109+ } ) ;
110+ fireEvent . change ( screen . getByLabelText ( / D e s t i n a t i o n a s s e t / i) , {
111+ target : { value : "EURC" } ,
112+ } ) ;
113+ fireEvent . change ( screen . getByLabelText ( / A m o u n t / i) , {
114+ target : { value : "100" } ,
115+ } ) ;
116+ fireEvent . submit ( screen . getByLabelText ( / A m o u n t / i) . closest ( "form" ) ! ) ;
117+
118+ await waitFor ( ( ) => {
119+ expect ( screen . getByRole ( "alert" ) ) . toHaveTextContent ( / 1 - 1 2 l e t t e r s o r n u m b e r s / i) ;
120+ } ) ;
121+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
122+ } ) ;
123+
124+ it ( "blocks submission when an asset code is over length" , async ( ) => {
125+ const mockFetch = jest . fn ( ) ;
126+ globalThis . fetch = mockFetch as unknown as typeof globalThis . fetch ;
127+ render ( < QuotePage /> ) ;
128+
129+ fireEvent . change ( screen . getByLabelText ( / S o u r c e a s s e t / i) , {
130+ target : { value : "TOO-LONG-ASSET" } ,
131+ } ) ;
132+ fireEvent . change ( screen . getByLabelText ( / D e s t i n a t i o n a s s e t / i) , {
133+ target : { value : "EURC" } ,
134+ } ) ;
135+ fireEvent . change ( screen . getByLabelText ( / A m o u n t / i) , {
136+ target : { value : "100" } ,
137+ } ) ;
138+ fireEvent . submit ( screen . getByLabelText ( / A m o u n t / i) . closest ( "form" ) ! ) ;
139+
140+ await waitFor ( ( ) => {
141+ expect ( screen . getByRole ( "alert" ) ) . toHaveTextContent ( / 1 - 1 2 l e t t e r s o r n u m b e r s / i) ;
142+ } ) ;
143+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
144+ } ) ;
145+
146+ it ( "blocks submission when asset codes are whitespace-only" , async ( ) => {
147+ const mockFetch = jest . fn ( ) ;
148+ globalThis . fetch = mockFetch as unknown as typeof globalThis . fetch ;
149+ render ( < QuotePage /> ) ;
150+
151+ fireEvent . change ( screen . getByLabelText ( / S o u r c e a s s e t / i) , {
152+ target : { value : " " } ,
153+ } ) ;
154+ fireEvent . change ( screen . getByLabelText ( / D e s t i n a t i o n a s s e t / i) , {
155+ target : { value : "EURC" } ,
156+ } ) ;
157+ fireEvent . change ( screen . getByLabelText ( / A m o u n t / i) , {
158+ target : { value : "100" } ,
159+ } ) ;
160+ fireEvent . submit ( screen . getByLabelText ( / A m o u n t / i) . closest ( "form" ) ! ) ;
161+
162+ await waitFor ( ( ) => {
163+ expect ( screen . getByRole ( "alert" ) ) . toHaveTextContent ( / 1 - 1 2 l e t t e r s o r n u m b e r s / i) ;
164+ } ) ;
165+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
166+ } ) ;
167+
168+ it ( "trims valid asset codes and amount before issuing the request" , async ( ) => {
169+ const mockFetch = jest . fn ( ) . mockResolvedValueOnce ( {
170+ ok : true ,
171+ json : async ( ) => ( {
172+ source_asset : "USDC" ,
173+ dest_asset : "EURC" ,
174+ amount : "100" ,
175+ estimated_rate : "1.0" ,
176+ route : [ "USDC" , "EURC" ] ,
177+ } ) ,
178+ } as unknown as Response ) ;
179+ globalThis . fetch = mockFetch as unknown as typeof globalThis . fetch ;
180+ render ( < QuotePage /> ) ;
181+
182+ fireEvent . change ( screen . getByLabelText ( / S o u r c e a s s e t / i) , {
183+ target : { value : " USDC " } ,
184+ } ) ;
185+ fireEvent . change ( screen . getByLabelText ( / D e s t i n a t i o n a s s e t / i) , {
186+ target : { value : " EURC " } ,
187+ } ) ;
188+ fireEvent . change ( screen . getByLabelText ( / A m o u n t / i) , {
189+ target : { value : " 100 " } ,
190+ } ) ;
191+ fireEvent . click ( screen . getByRole ( "button" , { name : / G e t q u o t e / i } ) ) ;
192+
193+ await waitFor ( ( ) => {
194+ expect ( screen . getByRole ( "status" ) ) . toHaveTextContent ( / U S D C → E U R C / ) ;
195+ } ) ;
196+ expect ( mockFetch ) . toHaveBeenCalledWith (
197+ expect . stringContaining ( "source_asset=USDC&dest_asset=EURC&amount=100" ) ,
198+ ) ;
199+ } ) ;
200+
98201 it ( "surfaces a backend invalid_request as a role=alert" , async ( ) => {
99202 globalThis . fetch = jest . fn ( ) . mockResolvedValueOnce ( {
100203 ok : false ,
0 commit comments