@@ -145,6 +145,116 @@ describe "Macros" do
145
145
response.status_code.should eq(200 )
146
146
response.headers[" Content-Disposition" ].should eq(" attachment; filename=\" image.jpg\" " )
147
147
end
148
+
149
+ it " handles multiple range requests" do
150
+ get " /" do |env |
151
+ send_file env, " #{ __DIR__ } /asset/hello.ecr"
152
+ end
153
+
154
+ headers = HTTP ::Headers {" Range" => " bytes=0-4,7-11" }
155
+ request = HTTP ::Request .new(" GET" , " /" , headers)
156
+ response = call_request_on_app(request)
157
+
158
+ response.status_code.should eq(206 )
159
+ response.headers[" Content-Type" ].should match(/^multipart\/ byteranges; boundary=kemal-/ )
160
+ response.headers[" Accept-Ranges" ].should eq(" bytes" )
161
+
162
+ # Verify multipart response structure
163
+ body = response.body
164
+ boundary = response.headers[" Content-Type" ].split(" boundary=" )[1 ]
165
+ parts = body.split(" --#{ boundary } " )
166
+ # Parts structure:
167
+ # 1. Empty part before first boundary
168
+ # 2. First content part (0-4)
169
+ # 3. Second content part (7-11)
170
+ # 4. Trailing part after last boundary
171
+ parts.size.should eq(4 )
172
+
173
+ # First part (0-4)
174
+ first_part = parts[1 ]
175
+ first_part.should contain(" Content-Type: multipart/byteranges" )
176
+ first_part.should contain(" Content-Range: bytes 0-4/18" )
177
+ first_part.split(" \r\n\r\n " )[1 ].strip.should eq(" Hello" )
178
+
179
+ # Second part (7-11)
180
+ second_part = parts[2 ]
181
+ second_part.should contain(" Content-Type: multipart/byteranges" )
182
+ second_part.should contain(" Content-Range: bytes 7-11/18" )
183
+ second_part.split(" \r\n\r\n " )[1 ].strip.should eq(" %= na" )
184
+ end
185
+
186
+ it " handles invalid range requests" do
187
+ get " /" do |env |
188
+ send_file env, " #{ __DIR__ } /asset/hello.ecr"
189
+ end
190
+
191
+ # Invalid range format
192
+ headers = HTTP ::Headers {" Range" => " invalid" }
193
+ request = HTTP ::Request .new(" GET" , " /" , headers)
194
+ response = call_request_on_app(request)
195
+ response.status_code.should eq(200 )
196
+ response.body.should eq(File .read(" #{ __DIR__ } /asset/hello.ecr" ))
197
+
198
+ # Range out of bounds
199
+ headers = HTTP ::Headers {" Range" => " bytes=100-200" }
200
+ request = HTTP ::Request .new(" GET" , " /" , headers)
201
+ response = call_request_on_app(request)
202
+ response.status_code.should eq(200 )
203
+ response.body.should eq(File .read(" #{ __DIR__ } /asset/hello.ecr" ))
204
+
205
+ # Invalid range values
206
+ headers = HTTP ::Headers {" Range" => " bytes=5-3" }
207
+ request = HTTP ::Request .new(" GET" , " /" , headers)
208
+ response = call_request_on_app(request)
209
+ response.status_code.should eq(200 )
210
+ response.body.should eq(File .read(" #{ __DIR__ } /asset/hello.ecr" ))
211
+ end
212
+
213
+ it " handles empty range requests" do
214
+ get " /" do |env |
215
+ send_file env, " #{ __DIR__ } /asset/hello.ecr"
216
+ end
217
+
218
+ headers = HTTP ::Headers {" Range" => " bytes=" }
219
+ request = HTTP ::Request .new(" GET" , " /" , headers)
220
+ response = call_request_on_app(request)
221
+ response.status_code.should eq(200 )
222
+ response.body.should eq(File .read(" #{ __DIR__ } /asset/hello.ecr" ))
223
+ end
224
+
225
+ it " handles overlapping ranges" do
226
+ get " /" do |env |
227
+ send_file env, " #{ __DIR__ } /asset/hello.ecr"
228
+ end
229
+
230
+ headers = HTTP ::Headers {" Range" => " bytes=0-5,3-8" }
231
+ request = HTTP ::Request .new(" GET" , " /" , headers)
232
+ response = call_request_on_app(request)
233
+
234
+ response.status_code.should eq(206 )
235
+ response.headers[" Content-Type" ].should match(/^multipart\/ byteranges; boundary=kemal-/ )
236
+
237
+ # Verify both ranges are included
238
+ body = response.body
239
+ boundary = response.headers[" Content-Type" ].split(" boundary=" )[1 ]
240
+ parts = body.split(" --#{ boundary } " )
241
+ # Parts structure:
242
+ # 1. Empty part before first boundary
243
+ # 2. First content part (0-5)
244
+ # 3. Second content part (3-8)
245
+ # 4. Trailing part after last boundary
246
+ parts.size.should eq(4 )
247
+
248
+ # First part (0-5)
249
+ first_part = parts[1 ]
250
+ first_part.should contain(" Content-Range: bytes 0-5/18" )
251
+ first_part.split(" \r\n\r\n " )[1 ].strip.should eq(" Hello" )
252
+
253
+ # Second part (3-8)
254
+ second_part = parts[2 ]
255
+ second_part.should contain(" Content-Range: bytes 3-8/18" )
256
+ second_part.split(" \r\n\r\n " )[1 ].strip.should eq(" lo <%=" )
257
+ end
148
258
end
149
259
150
260
describe " #gzip" do
0 commit comments