ftpsite_agent_spec.rb 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. require 'rails_helper'
  2. require 'time'
  3. describe Agents::FtpsiteAgent do
  4. describe "checking anonymous FTP" do
  5. before do
  6. @site = {
  7. 'expected_update_period_in_days' => 1,
  8. 'url' => "ftp://ftp.example.org/pub/releases/",
  9. 'patterns' => ["example*.tar.gz"],
  10. 'mode' => 'read',
  11. 'filename' => 'test',
  12. 'data' => '{{ data }}'
  13. }
  14. @checker = Agents::FtpsiteAgent.new(:name => "Example", :options => @site, :keep_events_for => 2.days)
  15. @checker.user = users(:bob)
  16. @checker.save!
  17. end
  18. context "#validate_options" do
  19. it "requires url to be a valid URI" do
  20. @checker.options['url'] = 'not_valid'
  21. expect(@checker).not_to be_valid
  22. end
  23. it "allows an URI without a path" do
  24. @checker.options['url'] = 'ftp://ftp.example.org'
  25. expect(@checker).to be_valid
  26. end
  27. it "does not check the url when liquid output markup is used" do
  28. @checker.options['url'] = 'ftp://{{ ftp_host }}'
  29. expect(@checker).to be_valid
  30. end
  31. it "requires patterns to be present and not empty array" do
  32. @checker.options['patterns'] = ''
  33. expect(@checker).not_to be_valid
  34. @checker.options['patterns'] = 'not an array'
  35. expect(@checker).not_to be_valid
  36. @checker.options['patterns'] = []
  37. expect(@checker).not_to be_valid
  38. end
  39. it "when present timestamp must be parsable into a Time object instance" do
  40. @checker.options['timestamp'] = '2015-01-01 00:00:01'
  41. expect(@checker).to be_valid
  42. @checker.options['timestamp'] = 'error'
  43. expect(@checker).not_to be_valid
  44. end
  45. it "requires mode to be set to 'read' or 'write'" do
  46. @checker.options['mode'] = 'write'
  47. expect(@checker).to be_valid
  48. @checker.options['mode'] = ''
  49. expect(@checker).not_to be_valid
  50. end
  51. it 'automatically sets mode to read when the agent is a new record' do
  52. checker = Agents::FtpsiteAgent.new(name: 'test', options: @site.except('mode'))
  53. checker.user = users(:bob)
  54. expect(checker).to be_valid
  55. expect(checker.options['mode']).to eq('read')
  56. end
  57. it "requires 'filename' in 'write' mode" do
  58. @checker.options['mode'] = 'write'
  59. @checker.options['filename'] = ''
  60. expect(@checker).not_to be_valid
  61. end
  62. it "requires 'data' in 'write' mode" do
  63. @checker.options['mode'] = 'write'
  64. @checker.options['data'] = ''
  65. expect(@checker).not_to be_valid
  66. end
  67. end
  68. describe "#check" do
  69. before do
  70. allow(@checker).to receive(:each_entry) { |&block|
  71. block.call("example latest.tar.gz", Time.parse("2014-04-01T10:00:01Z"))
  72. block.call("example-1.0.tar.gz", Time.parse("2013-10-01T10:00:00Z"))
  73. block.call("example-1.1.tar.gz", Time.parse("2014-04-01T10:00:00Z"))
  74. }
  75. end
  76. it "should validate the integer fields" do
  77. @checker.options['expected_update_period_in_days'] = "nonsense"
  78. expect { @checker.save! }.to raise_error(/Invalid expected_update_period_in_days format/);
  79. @checker.options = @site
  80. end
  81. it "should check for changes and save known entries in memory" do
  82. expect { @checker.check }.to change { Event.count }.by(3)
  83. @checker.memory['known_entries'].tap { |known_entries|
  84. expect(known_entries.size).to eq(3)
  85. expect(known_entries.sort_by(&:last)).to eq([
  86. ["example-1.0.tar.gz", "2013-10-01T10:00:00Z"],
  87. ["example-1.1.tar.gz", "2014-04-01T10:00:00Z"],
  88. ["example latest.tar.gz", "2014-04-01T10:00:01Z"],
  89. ])
  90. }
  91. expect(Event.last(2).first.payload).to eq({
  92. 'file_pointer' => { 'file' => 'example-1.1.tar.gz', 'agent_id' => @checker.id },
  93. 'url' => 'ftp://ftp.example.org/pub/releases/example-1.1.tar.gz',
  94. 'filename' => 'example-1.1.tar.gz',
  95. 'timestamp' => '2014-04-01T10:00:00Z',
  96. })
  97. expect { @checker.check }.not_to change { Event.count }
  98. allow(@checker).to receive(:each_entry) { |&block|
  99. block.call("example latest.tar.gz", Time.parse("2014-04-02T10:00:01Z"))
  100. # In the long list format the timestamp may look going
  101. # backwards after six months: Oct 01 10:00 -> Oct 01 2013
  102. block.call("example-1.0.tar.gz", Time.parse("2013-10-01T00:00:00Z"))
  103. block.call("example-1.1.tar.gz", Time.parse("2014-04-01T10:00:00Z"))
  104. block.call("example-1.2.tar.gz", Time.parse("2014-04-02T10:00:00Z"))
  105. }
  106. expect { @checker.check }.to change { Event.count }.by(2)
  107. @checker.memory['known_entries'].tap { |known_entries|
  108. expect(known_entries.size).to eq(4)
  109. expect(known_entries.sort_by(&:last)).to eq([
  110. ["example-1.0.tar.gz", "2013-10-01T00:00:00Z"],
  111. ["example-1.1.tar.gz", "2014-04-01T10:00:00Z"],
  112. ["example-1.2.tar.gz", "2014-04-02T10:00:00Z"],
  113. ["example latest.tar.gz", "2014-04-02T10:00:01Z"],
  114. ])
  115. }
  116. expect(Event.last(2).first.payload).to eq({
  117. 'file_pointer' => { 'file' => 'example-1.2.tar.gz', 'agent_id' => @checker.id },
  118. 'url' => 'ftp://ftp.example.org/pub/releases/example-1.2.tar.gz',
  119. 'filename' => 'example-1.2.tar.gz',
  120. 'timestamp' => '2014-04-02T10:00:00Z',
  121. })
  122. expect(Event.last.payload).to eq({
  123. 'file_pointer' => { 'file' => 'example latest.tar.gz', 'agent_id' => @checker.id },
  124. 'url' => 'ftp://ftp.example.org/pub/releases/example%20latest.tar.gz',
  125. 'filename' => 'example latest.tar.gz',
  126. 'timestamp' => '2014-04-02T10:00:01Z',
  127. })
  128. expect { @checker.check }.not_to change { Event.count }
  129. end
  130. end
  131. describe "#each_entry" do
  132. before do
  133. allow_any_instance_of(Net::FTP).to receive(:list).and_return [ # Windows format
  134. "04-02-14 10:01AM 288720748 example latest.tar.gz",
  135. "04-01-14 10:05AM 288720710 no-match-example.tar.gz"
  136. ]
  137. allow(@checker).to receive(:open_ftp).and_yield(Net::FTP.new)
  138. end
  139. it "filters out files that don't match the given format" do
  140. entries = []
  141. @checker.each_entry { |a, b| entries.push [a, b] }
  142. expect(entries.size).to eq(1)
  143. filename, mtime = entries.first
  144. expect(filename).to eq('example latest.tar.gz')
  145. expect(mtime).to eq('2014-04-02T10:01:00Z')
  146. end
  147. it "filters out files that are older than the given date" do
  148. @checker.options['after'] = '2015-10-21'
  149. entries = []
  150. @checker.each_entry { |a, b| entries.push [a, b] }
  151. expect(entries.size).to eq(0)
  152. end
  153. end
  154. context "#open_ftp" do
  155. before(:each) do
  156. @ftp_mock = double()
  157. allow(@ftp_mock).to receive(:close)
  158. allow(@ftp_mock).to receive(:connect).with('ftp.example.org', 21)
  159. allow(@ftp_mock).to receive(:passive=).with(true)
  160. allow(Net::FTP).to receive(:new) { @ftp_mock }
  161. end
  162. context 'with_path' do
  163. before(:each) { expect(@ftp_mock).to receive(:chdir).with('pub/releases') }
  164. it "logs in as anonymous when no user and password are given" do
  165. expect(@ftp_mock).to receive(:login).with('anonymous', 'anonymous@')
  166. expect { |b| @checker.open_ftp(@checker.base_uri, &b) }.to yield_with_args(@ftp_mock)
  167. end
  168. it "passes the provided user and password" do
  169. @checker.options['url'] = "ftp://user:password@ftp.example.org/pub/releases/"
  170. expect(@ftp_mock).to receive(:login).with('user', 'password')
  171. expect { |b| @checker.open_ftp(@checker.base_uri, &b) }.to yield_with_args(@ftp_mock)
  172. end
  173. end
  174. it "does not call chdir when no path is given" do
  175. @checker.options['url'] = "ftp://ftp.example.org/"
  176. expect(@ftp_mock).to receive(:login).with('anonymous', 'anonymous@')
  177. expect { |b| @checker.open_ftp(@checker.base_uri, &b) }.to yield_with_args(@ftp_mock)
  178. end
  179. end
  180. context "#get_io" do
  181. it "returns the contents of the file" do
  182. ftp_mock = double()
  183. expect(ftp_mock).to receive(:getbinaryfile).with('file', nil).and_yield('data')
  184. expect(@checker).to receive(:open_ftp).with(@checker.base_uri).and_yield(ftp_mock)
  185. expect(@checker.get_io('file').read).to eq('data')
  186. end
  187. it "uses the encoding specified in force_encoding to convert the data to UTF-8" do
  188. ftp_mock = double()
  189. expect(ftp_mock).to receive(:getbinaryfile).with('file', nil).and_yield('ümlaut'.force_encoding('ISO-8859-15'))
  190. expect(@checker).to receive(:open_ftp).with(@checker.base_uri).and_yield(ftp_mock)
  191. expect(@checker.get_io('file').read).to eq('ümlaut')
  192. end
  193. it "returns an empty StringIO instance when no data was read" do
  194. ftp_mock = double()
  195. expect(ftp_mock).to receive(:getbinaryfile).with('file', nil)
  196. expect(@checker).to receive(:open_ftp).with(@checker.base_uri).and_yield(ftp_mock)
  197. expect(@checker.get_io('file').length).to eq(0)
  198. end
  199. end
  200. context "#receive" do
  201. before(:each) do
  202. @checker.options['mode'] = 'write'
  203. @checker.options['filename'] = 'file.txt'
  204. @checker.options['data'] = '{{ data }}'
  205. @ftp_mock = double()
  206. @stringio = StringIO.new()
  207. allow(@checker).to receive(:open_ftp).with(@checker.base_uri).and_yield(@ftp_mock)
  208. end
  209. it "writes the data at data into a file" do
  210. expect(StringIO).to receive(:new).with('hello world🔥') { @stringio }
  211. expect(@ftp_mock).to receive(:storbinary).with('STOR file.txt', @stringio, Net::FTP::DEFAULT_BLOCKSIZE)
  212. event = Event.new(payload: {'data' => 'hello world🔥'})
  213. @checker.receive([event])
  214. end
  215. it "converts the string encoding when force_encoding is specified" do
  216. @checker.options['force_encoding'] = 'ISO-8859-1'
  217. expect(StringIO).to receive(:new).with('hello world?') { @stringio }
  218. expect(@ftp_mock).to receive(:storbinary).with('STOR file.txt', @stringio, Net::FTP::DEFAULT_BLOCKSIZE)
  219. event = Event.new(payload: {'data' => 'hello world🔥'})
  220. @checker.receive([event])
  221. end
  222. end
  223. end
  224. end