@@ -209,3 +209,90 @@ describe("BridgedRequestProvider.getImage", () => {
209209 await expect ( promise ) . rejects . toThrow ( "404 GET" ) ;
210210 } ) ;
211211} ) ;
212+
213+ describe ( "BridgedRequestProvider.fetchResource" , ( ) => {
214+ it ( "asks the native side for the bytes and reports what they came under" , async ( ) => {
215+ const provider = new BridgedRequestProvider ( ) ;
216+ const promise = provider . fetchResource ( "https://example.com/p" , {
217+ maxBytes : 1000 ,
218+ headers : { "user-agent" : "TriliumNotes" }
219+ } ) ;
220+
221+ expect ( lastPosted ( ) . request ) . toMatchObject ( {
222+ method : "GET" ,
223+ url : "https://example.com/p" ,
224+ headers : { "user-agent" : "TriliumNotes" } ,
225+ responseType : "arraybuffer"
226+ } ) ;
227+
228+ respond ( { status : 200 , headers : { "content-type" : "text/html; charset=utf-8" } , body : btoa ( "<html/>" ) } ) ;
229+
230+ await expect ( promise ) . resolves . toEqual ( {
231+ status : 200 ,
232+ ok : true ,
233+ contentType : "text/html" ,
234+ bytes : new TextEncoder ( ) . encode ( "<html/>" )
235+ } ) ;
236+ } ) ;
237+
238+ it ( "answers a non-2xx rather than throwing, the page having said something" , async ( ) => {
239+ const provider = new BridgedRequestProvider ( ) ;
240+ const promise = provider . fetchResource ( "https://example.com/gone" , { maxBytes : 1000 } ) ;
241+
242+ respond ( { status : 404 , headers : { } , body : btoa ( "nope" ) } ) ;
243+
244+ await expect ( promise ) . resolves . toMatchObject ( { status : 404 , ok : false , contentType : "" } ) ;
245+ } ) ;
246+
247+ it ( "refuses an address before anything is posted for it" , async ( ) => {
248+ const provider = new BridgedRequestProvider ( ) ;
249+ const posted = postSpy . mock . calls . length ;
250+
251+ await expect ( provider . fetchResource ( "file:///etc/passwd" , { maxBytes : 10 } ) ) . rejects . toThrow ( / h t t p a n d h t t p s / ) ;
252+ expect ( postSpy . mock . calls . length ) . toBe ( posted ) ;
253+ } ) ;
254+
255+ /**
256+ * The ceiling is checked on what arrived rather than on what is arriving — a native transport
257+ * hands over a whole response, so there is no stream to abandon. It still has to hold.
258+ */
259+ it ( "refuses a body over the ceiling once it is back" , async ( ) => {
260+ const provider = new BridgedRequestProvider ( ) ;
261+ const promise = provider . fetchResource ( "https://example.com/big" , { maxBytes : 10 } ) ;
262+
263+ respond ( { status : 200 , headers : { } , body : btoa ( "x" . repeat ( 50 ) ) } ) ;
264+
265+ await expect ( promise ) . rejects . toThrow ( / e x c e e d s t h e 1 0 b y t e l i m i t / ) ;
266+ } ) ;
267+
268+ /**
269+ * And refuses it *before* decoding, which is the part that matters. Decoding is where an
270+ * oversized body multiplies — atob builds a binary string and Uint8Array.from copies that again
271+ * — so a check running afterwards has already paid for what it is about to refuse.
272+ */
273+ it ( "refuses an oversized body without decoding it" , async ( ) => {
274+ const decode = vi . spyOn ( globalThis , "atob" ) ;
275+ const provider = new BridgedRequestProvider ( ) ;
276+ const promise = provider . fetchResource ( "https://example.com/big" , { maxBytes : 10 } ) ;
277+
278+ respond ( { status : 200 , headers : { } , body : btoa ( "x" . repeat ( 5000 ) ) } ) ;
279+
280+ await expect ( promise ) . rejects . toThrow ( / e x c e e d s t h e 1 0 b y t e l i m i t / ) ;
281+ expect ( decode ) . not . toHaveBeenCalled ( ) ;
282+ } ) ;
283+
284+ it ( "measures the decoded size rather than the encoded one, at every padding" , async ( ) => {
285+ // Base64 runs 4/3 the size of what it carries, so measuring the encoded string would refuse
286+ // a 30-byte body against a 30-byte ceiling. The three lengths cover the three paddings.
287+ for ( const length of [ 30 , 29 , 28 ] ) {
288+ const provider = new BridgedRequestProvider ( ) ;
289+ const promise = provider . fetchResource ( "https://example.com/exact" , { maxBytes : 30 } ) ;
290+
291+ respond ( { status : 200 , headers : { } , body : btoa ( "x" . repeat ( length ) ) } ) ;
292+
293+ await expect ( promise ) . resolves . toMatchObject ( {
294+ bytes : new Uint8Array ( length ) . fill ( "x" . charCodeAt ( 0 ) )
295+ } ) ;
296+ }
297+ } ) ;
298+ } ) ;
0 commit comments