1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
|
require 'spec_helper'
require 'tempfile'
RSpec.describe HTTParty::Request::StreamingMultipartBody do
let(:boundary) { '------------------------c772861a5109d5ef' }
describe '#read' do
context 'with a simple file' do
let(:file) { File.open('spec/fixtures/tiny.gif', 'rb') }
let(:parts) { [['avatar', file, true]] }
subject { described_class.new(parts, boundary) }
after { file.close }
it 'streams the complete multipart body' do
result = subject.read
expect(result.encoding).to eq(Encoding::BINARY)
expect(result).to include("--#{boundary}")
expect(result).to include('Content-Disposition: form-data; name="avatar"')
expect(result).to include('filename="tiny.gif"')
expect(result).to include('Content-Type: image/gif')
expect(result).to include("GIF89a") # GIF file header
expect(result).to end_with("--#{boundary}--\r\n")
end
it 'returns same content as non-streaming body' do
# Create equivalent Body for comparison
body = HTTParty::Request::Body.new({ avatar: File.open('spec/fixtures/tiny.gif', 'rb') })
allow(HTTParty::Request::MultipartBoundary).to receive(:generate).and_return(boundary)
streaming_result = subject.read
non_streaming_result = body.call
expect(streaming_result).to eq(non_streaming_result)
end
end
context 'with mixed file and text fields' do
let(:file) { File.open('spec/fixtures/tiny.gif', 'rb') }
let(:parts) do
[
['user[avatar]', file, true],
['user[name]', 'John Doe', false],
['user[active]', 'true', false]
]
end
subject { described_class.new(parts, boundary) }
after { file.close }
it 'streams all parts correctly' do
result = subject.read
expect(result).to include('name="user[avatar]"')
expect(result).to include('name="user[name]"')
expect(result).to include('John Doe')
expect(result).to include('name="user[active]"')
expect(result).to include('true')
end
end
context 'reading in chunks' do
let(:file) { File.open('spec/fixtures/tiny.gif', 'rb') }
let(:parts) { [['avatar', file, true]] }
subject { described_class.new(parts, boundary) }
after { file.close }
it 'reads correctly in small chunks' do
chunks = []
while (chunk = subject.read(10))
chunks << chunk
end
full_result = chunks.join
subject.rewind
single_read = subject.read
expect(full_result).to eq(single_read)
end
it 'returns nil when exhausted' do
subject.read # Read all
expect(subject.read).to be_nil
end
end
end
describe '#size' do
let(:file) { File.open('spec/fixtures/tiny.gif', 'rb') }
let(:parts) { [['avatar', file, true]] }
subject { described_class.new(parts, boundary) }
after { file.close }
it 'returns the correct total size' do
size = subject.size
content = subject.read
expect(size).to eq(content.bytesize)
end
end
describe '#rewind' do
let(:file) { File.open('spec/fixtures/tiny.gif', 'rb') }
let(:parts) { [['avatar', file, true]] }
subject { described_class.new(parts, boundary) }
after { file.close }
it 'allows re-reading the stream' do
first_read = subject.read
subject.rewind
second_read = subject.read
expect(first_read).to eq(second_read)
end
end
describe 'memory efficiency' do
it 'does not load entire file into memory at once' do
# Create a larger temp file
tempfile = Tempfile.new(['large', '.bin'])
tempfile.write('x' * (1024 * 1024)) # 1 MB
tempfile.rewind
parts = [['file', tempfile, true]]
stream = described_class.new(parts, boundary)
# Read in small chunks - this should work without allocating 1MB at once
chunks_read = 0
while stream.read(1024)
chunks_read += 1
end
expect(chunks_read).to be > 100 # Should have read many chunks
tempfile.close
tempfile.unlink
end
end
end
|