PageRenderTime 71ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/spec/data/repo_spec.rb

https://bitbucket.org/rick_stokkingreef/razor-server
Ruby | 620 lines | 536 code | 54 blank | 30 comment | 15 complexity | e478a85179f50399c697d63ef09cc9dc MD5 | raw file
Possible License(s): Apache-2.0, GPL-2.0
  1. # -*- encoding: utf-8 -*-
  2. # coding: utf-8
  3. require_relative "../spec_helper"
  4. require 'tempfile'
  5. require 'tmpdir'
  6. require 'webrick'
  7. describe Razor::Data::Repo do
  8. include TorqueBox::Injectors
  9. let :queue do fetch('/queues/razor/sequel-instance-messages') end
  10. context "name" do
  11. (0..31).map {|n| n.chr(Encoding::UTF_8) }.map(&:to_s).each do |char|
  12. it "should reject control characters (testing: #{char.inspect})" do
  13. expect do
  14. repo = Fabricate(:repo, :name => "hello #{char} world")
  15. end.to raise_error Sequel::ValidationFailed
  16. end
  17. end
  18. it "should reject the `/` character in names" do
  19. expect do
  20. repo = Fabricate(:repo, :name => "hello/goodbye")
  21. end.to raise_error Sequel::ValidationFailed
  22. end
  23. # A list of Unicode 6.0 whitespace, yay!
  24. [
  25. ?\u0009, # horizontal tab
  26. ?\u000A, # newline
  27. ?\u000B, # vertical tab
  28. ?\u000C, # new page
  29. ?\u000D, # carriage return
  30. ?\u0020, # space
  31. ?\u0085, # NEL, Next line
  32. ?\u00A0, # no-break space
  33. ?\u1680, # ogham space mark
  34. ?\u180E, # mongolian vowel separator
  35. ?\u2000, # en quad
  36. ?\u2001, # em quad
  37. ?\u2002, # en space
  38. ?\u2003, # em space
  39. ?\u2004, # three-per-em
  40. ?\u2005, # four-per-em
  41. ?\u2006, # six-per-em
  42. ?\u2007, # figure space
  43. ?\u2008, # punctuation space
  44. ?\u2009, # thin space
  45. ?\u200A, # hair space
  46. ?\u2028, # line separator
  47. ?\u2029, # paragraph separator
  48. ?\u202F, # narrow no-break space
  49. ?\u205F, # medium mathematical space
  50. ?\u3000 # ideographic space
  51. ].each do |ws|
  52. context "with whitespace (#{format('\u%04x', ws.ord)})" do
  53. url = 'file:///dev/null'
  54. context "in Ruby" do
  55. it "should be rejected at the start" do
  56. Fabricate.build(:repo, :name => "#{ws}name").should_not be_valid
  57. end
  58. it "should be rejected at the end" do
  59. Fabricate.build(:repo, :name => "name#{ws}").should_not be_valid
  60. end
  61. # Fair warning: what with using a regex for validation, this is a
  62. # common failure mode, and not in fact redundant to the checks above.
  63. it "should be rejected at both the start and the end" do
  64. Fabricate.build(:repo, :name => "#{ws}name#{ws}").should_not be_valid
  65. end
  66. if ws.ord >= 0x20 then
  67. it "should accept the whitespace in the middle of a name" do
  68. Fabricate.build(:repo, :name => "hello#{ws}world").
  69. should be_valid
  70. end
  71. end
  72. end
  73. context "in PostgreSQL" do
  74. it "should be rejected at the start" do
  75. expect {
  76. Repo.dataset.insert(Fabricate.build(:repo, :name => "#{ws}name"))
  77. }.to raise_error Sequel::CheckConstraintViolation
  78. end
  79. it "should be rejected at the end" do
  80. expect {
  81. Repo.dataset.insert(Fabricate.build(:repo, :name => "name#{ws}"))
  82. }.to raise_error Sequel::CheckConstraintViolation
  83. end
  84. # Fair warning: what with using a regex for validation, this is a
  85. # common failure mode, and not in fact redundant to the checks above.
  86. it "should be rejected at both the start and the end" do
  87. expect {
  88. Repo.dataset.insert(Fabricate.build(:repo, :name => "#{ws}name#{ws}"))
  89. }.to raise_error Sequel::CheckConstraintViolation
  90. end
  91. if ws.ord >= 0x20 then
  92. it "should accept the whitespace in the middle of a name" do
  93. # As long as we don't raise, we win.
  94. Fabricate(:repo, :name => "hello#{ws}world")
  95. end
  96. end
  97. end
  98. end
  99. end
  100. # Using 32 characters at a time here is a trade-off: it is much faster
  101. # than running validation on each character uniquely, which has a fairly
  102. # high start-up overhead. On the other hand, with the shuffle it gives
  103. # reasonable statistical probability that a flaw in the validation will
  104. # eventually be captured. Given we report the PRNG seed, we can also
  105. # reproduce the test... this does require Ruby 1.9 to function.
  106. # --daniel 2013-06-24
  107. prng = Random.new
  108. context "statistical validation with prng: #{prng.seed}" do
  109. banned = [
  110. 0x0009, # horizontal tab
  111. 0x000A, # newline
  112. 0x000B, # vertical tab
  113. 0x000C, # new page
  114. 0x000D, # carriage return
  115. 0x0020, # space
  116. 0x002F, # forward slash
  117. 0x0085, # NEL, Next line
  118. 0x00A0, # no-break space
  119. 0x1680, # ogham space mark
  120. 0x180E, # mongolian vowel separator
  121. 0x2000, # en quad
  122. 0x2001, # em quad
  123. 0x2002, # en space
  124. 0x2003, # em space
  125. 0x2004, # three-per-em
  126. 0x2005, # four-per-em
  127. 0x2006, # six-per-em
  128. 0x2007, # figure space
  129. 0x2008, # punctuation space
  130. 0x2009, # thin space
  131. 0x200A, # hair space
  132. 0x2028, # line separator
  133. 0x2029, # paragraph separator
  134. 0x202F, # narrow no-break space
  135. 0x205F, # medium mathematical space
  136. 0x3000 # ideographic space
  137. ]
  138. (32..0x266b).
  139. reject{|x| banned.member? x }.
  140. shuffle(random: prng).
  141. each_slice(32) do |c|
  142. string = c.map{|n| n.chr(Encoding::UTF_8)}.join('')
  143. display = "\\u{#{c.map{|n| n.to_s(16)}.join(' ')}}"
  144. # If you came here seeking understanding, the `\u{1 2 3}` form is a
  145. # nice way of escaping the characters so that your terminal doesn't
  146. # spend a while loading literally *every* Unicode code point from
  147. # fallback fonts when you use, eg, the documentation formatter.
  148. #
  149. # Internally this is testing on the actual *characters*.
  150. it "accept all legal characters: string \"#{display}\"" do
  151. Fabricate(:repo, :name => string).save.should be_valid
  152. end
  153. end
  154. end
  155. context "aggressive Unicode support" do
  156. [ # You are not expected to understand this text.
  157. "ÆtherÜnikérûn", "काkāὕαΜπΜπ", "Pòîê᚛᚛ᚉᚑᚅᛁ", "ᚳ᛫æðþȝaɪkæ", "n⠊⠉⠁ᛖᚴярса",
  158. "нЯškłoმინა", "სԿրնամجامআ", "মিमीकನನಗका", "चநான்నేనుම", "ටවීکانشيشم",
  159. "یأناאנאיɜn", "yɜإِنက္ယ္q", "uốcngữ些世ខ្", "ញຂອ້ຍฉันกิ", "मकाཤེལ我能吞我",
  160. "能私はガラ나는유리ᓂ", "ᕆᔭᕌᖓ",
  161. ].each do |name|
  162. url = 'file:///dev/null'
  163. it "should accept the name #{name.inspect}" do
  164. Fabricate(:repo).set(:iso_url => url, :url => nil).should be_valid
  165. end
  166. it "should round-trip the name #{name.inspect} through the database" do
  167. repo = Fabricate(:repo, :name => name).save
  168. Repo.find(:name => name).should == repo
  169. end
  170. end
  171. end
  172. end
  173. [:url, :iso_url].each do |url_name|
  174. context url_name.to_s do
  175. [
  176. 'http://example.com/foobar',
  177. 'http://example/foobar',
  178. 'http://example.com/',
  179. 'http://example.com',
  180. 'https://foo.example.com/repo.iso',
  181. 'file:/dev/null',
  182. 'file:///dev/null'
  183. ].each do |url|
  184. it "should accept a basic URL #{url.inspect}" do
  185. # save to push validation through the database, too.
  186. Fabricate.build(:repo).set('url' => nil, 'iso_url' => nil, url_name => url).save.should be_valid
  187. end
  188. end
  189. [
  190. 'ftp://example.com/foo.iso',
  191. 'file://example.com/dev/null',
  192. 'file://localhost/dev/null',
  193. 'http:///vmware.iso',
  194. 'https:///vmware.iso',
  195. "http://example.com/foo\tbar",
  196. "http://example.com/foo\nbar",
  197. "http://example.com/foo\n",
  198. 'http://example.com/foo bar'
  199. ].each do |url|
  200. it "Ruby should reject invalid URL #{url.inspect}" do
  201. Fabricate.build(:repo).set(:iso_url => url, :url => nil).should_not be_valid
  202. end
  203. it "PostgreSQL should reject invalid URL #{url.inspect}" do
  204. expect {
  205. Repo.dataset.insert(Fabricate.build(:repo, :iso_url => url))
  206. }.to raise_error Sequel::CheckConstraintViolation
  207. end
  208. end
  209. end
  210. end
  211. context "url and iso_url" do
  212. it "should reject setting both" do
  213. expect do
  214. Fabricate(:repo, :url => 'http://example.org/', :iso_url => 'http://example.com')
  215. end.to raise_error(Sequel::ValidationFailed)
  216. end
  217. it "should require setting one of them" do
  218. repo = Fabricate.build(:repo).set(:url => nil, :iso_url => nil).should_not be_valid
  219. end
  220. end
  221. context "task" do
  222. it "should store task_name" do
  223. repo = Fabricate(:repo, :task_name => 'microkernel')
  224. repo.task.name.should == 'microkernel'
  225. end
  226. it "should reject input with nil task_name" do
  227. expect { Fabricate(:repo, :task_name => nil) }.to raise_error(Sequel::InvalidValue)
  228. end
  229. end
  230. context "after creation" do
  231. it "should automatically 'make_the_repo_accessible'" do
  232. data = Fabricate.build(:repo).to_hash
  233. command = Fabricate(:command)
  234. expect { Razor::Data::Repo.import(data, command) }.
  235. to have_published(
  236. 'class' => Razor::Data::Repo.name,
  237. # Because we can't look into the future and see what that the PK will
  238. # be without saving, but we can't save without publishing the message
  239. # and spoiling the test, we have to check this more liberally...
  240. 'instance' => include(:id => be),
  241. 'command' => { :id => command.id },
  242. 'message' => 'make_the_repo_accessible'
  243. ).on(queue)
  244. command.reload
  245. command.status.should == 'pending'
  246. end
  247. end
  248. context "make_the_repo_accessible" do
  249. context "with file URLs" do
  250. let :tmpfile do Tempfile.new(['make_the_repo_accessible', '.iso']) end
  251. let :path do tmpfile.path end
  252. let :repo do Fabricate(:repo, :iso_url => "file://#{path}") end
  253. let :command do Fabricate(:command) end
  254. it "should raise (to trigger a retry) if the repo is not readable" do
  255. File.chmod(00000, path) # yes, *no* permissions, thanks
  256. expect {
  257. repo.make_the_repo_accessible(command)
  258. }.to raise_error RuntimeError, /unable to read local file/
  259. command.status.should == 'running'
  260. end
  261. it "should publish 'unpack_repo' if the repo is readable" do
  262. expect {
  263. repo.make_the_repo_accessible(command)
  264. }.to have_published(
  265. 'class' => repo.class.name,
  266. 'instance' => repo.pk_hash,
  267. 'command' => { :id => command.id },
  268. 'message' => 'unpack_repo',
  269. 'arguments' => [path]
  270. ).on(queue)
  271. end
  272. it "should work with uppercase file scheme" do
  273. repo.iso_url = "FILE://#{path}"
  274. expect {
  275. repo.make_the_repo_accessible(command)
  276. }.to have_published(
  277. 'class' => repo.class.name,
  278. 'instance' => repo.pk_hash,
  279. 'command' => { :id => command.id },
  280. 'message' => 'unpack_repo',
  281. 'arguments' => [path]
  282. ).on(queue)
  283. end
  284. end
  285. context "with HTTP URLs" do
  286. FileContent = "This is the file content.\n"
  287. LongFileSize = (Razor::Data::Repo::BufferSize * 2.5).ceil
  288. # around hooks don't allow us to use :all, and we only want to do
  289. # setup/teardown of this fixture once; since the server is stateless we
  290. # don't risk much doing so.
  291. before :all do
  292. null = WEBrick::Log.new('/dev/null')
  293. @server = WEBrick::HTTPServer.new(
  294. :Port => 8000,
  295. :Logger => null,
  296. :AccessLog => null,
  297. )
  298. @server.mount_proc '/short.iso' do |req, res|
  299. res.status = 200
  300. res.body = FileContent
  301. end
  302. @server.mount_proc '/long.iso' do |req, res|
  303. res.status = 200
  304. res.body = ' ' * LongFileSize
  305. end
  306. @server.mount_proc '/redirect.iso' do |req, res|
  307. res.status = 301
  308. res['location'] = '/long.iso'
  309. end
  310. Thread.new { @server.start }
  311. end
  312. after :all do
  313. @server and @server.shutdown
  314. end
  315. let :repo do Fabricate(:repo) end
  316. let :command do Fabricate(:command) end
  317. after :each do
  318. repo.exists? && repo.destroy
  319. end
  320. context "download_file_to_tempdir" do
  321. it "should raise (for retry) if the requested URL does not exist" do
  322. expect {
  323. repo.download_file_to_tempdir(URI.parse('http://localhost:8000/no-such-file'))
  324. }.to raise_error OpenURI::HTTPError, /404/
  325. end
  326. it "should copy short content down on success" do
  327. url = URI.parse('http://localhost:8000/short.iso')
  328. file = repo.download_file_to_tempdir(url)
  329. File.read(file).should == FileContent
  330. end
  331. it "should copy long content down on success" do
  332. url = URI.parse('http://localhost:8000/long.iso')
  333. file = repo.download_file_to_tempdir(url)
  334. File.size?(file).should == LongFileSize
  335. end
  336. it "should follow redirects" do
  337. url = URI.parse('http://localhost:8000/redirect.iso')
  338. file = repo.download_file_to_tempdir(url)
  339. File.size?(file).should == LongFileSize
  340. end
  341. end
  342. it "should publish 'unpack_repo' if the repo is readable" do
  343. repo.iso_url = 'http://localhost:8000/short.iso'
  344. repo.save # make sure our primary key is set!
  345. expect {
  346. repo.make_the_repo_accessible(command)
  347. }.to have_published(
  348. 'class' => repo.class.name,
  349. 'instance' => repo.pk_hash,
  350. 'command' => { :id => command.id },
  351. 'message' => 'unpack_repo',
  352. 'arguments' => [end_with('/short.iso')]
  353. ).on(queue)
  354. end
  355. end
  356. end
  357. context "on destroy" do
  358. it "should remove the temporary directory, if there is one" do
  359. tmpdir = Dir.mktmpdir('razor-repo-download')
  360. repo = Fabricate.build(:repo)
  361. repo.tmpdir = tmpdir
  362. repo.save
  363. repo.destroy
  364. File.should_not be_exist tmpdir
  365. end
  366. it "should remove the repo's unpacked iso directory" do
  367. tiny_iso = (Pathname(__FILE__).dirname.parent + 'fixtures' + 'iso' + 'tiny.iso').to_s
  368. command = Fabricate(:command)
  369. begin
  370. repo_dir = Dir.mktmpdir('test-razor-repo-dir')
  371. Razor.config.stub(:[]).with('repo_store_root').and_return(repo_dir)
  372. repo = Fabricate.build(:repo)
  373. repo.unpack_repo(command, tiny_iso)
  374. unpacked_iso_dir = File::join(repo_dir, repo.name)
  375. Dir.exist?(unpacked_iso_dir).should be_true
  376. repo.save
  377. repo.destroy
  378. Dir.exist?(unpacked_iso_dir).should be_false
  379. ensure
  380. # Cleanup
  381. repo_dir and FileUtils.remove_entry_secure(repo_dir)
  382. end
  383. end
  384. it "should not fail if there is no temporary directory" do
  385. repo = Fabricate.build(:repo)
  386. repo.tmpdir = nil
  387. repo.save
  388. repo.destroy
  389. end
  390. end
  391. context "filesystem_safe_name" do
  392. "\x00\x1f\x7f/\\?*:|\"<>$\',".each_char do |char|
  393. it "should escape #{char.inspect}" do
  394. repo = Fabricate.build(:repo, :name => "foo#{char}bar")
  395. repo.filesystem_safe_name.should_not include char
  396. repo.filesystem_safe_name.should =~ /%0{0,6}#{char.ord.to_s(16)}/i
  397. end
  398. end
  399. it "should escape '%' in filenames" do
  400. Fabricate.build(:repo, :name => '%ab').filesystem_safe_name.should == '%25ab'
  401. Fabricate.build(:repo, :name => 'a%b').filesystem_safe_name.should == 'a%25b'
  402. Fabricate.build(:repo, :name => 'ab%').filesystem_safe_name.should == 'ab%25'
  403. end
  404. Razor::Data::Repo::ReservedFilenames.each do |name|
  405. encoded = /#{name.upcase.gsub(/./) {|x| '%%0{0,6}%02X' % x.ord }}/i
  406. it "should encode reserved filename #{name.inspect}" do
  407. Fabricate.build(:repo, :name => name).filesystem_safe_name.should =~ encoded
  408. end
  409. it "should not encode files that end with #{name.inspect}" do
  410. Fabricate.build(:repo, :name => "s" + name).filesystem_safe_name.should == 's' + name
  411. end
  412. it "should not encode files that start with #{name.inspect} and anything but '.'" do
  413. Fabricate.build(:repo, :name => name + 's').filesystem_safe_name.should == name + 's'
  414. end
  415. %w{. .foo .con .txt .banana}.each do |ext|
  416. it "should encode reserved filename #{name.inspect} if followed by #{ext.inspect}" do
  417. Fabricate.build(:repo, :name => name + ext).filesystem_safe_name.
  418. should =~ /#{encoded}#{Regexp.escape(ext)}/
  419. end
  420. end
  421. it "should encode all possible case variants of #{name}" do
  422. bits = name.split('').map {|c| [c.downcase, c.upcase]}
  423. names = bits.first.product(*bits[1..-1]).map(&:join)
  424. names.each do |n|
  425. Fabricate.build(:repo, :name => name).filesystem_safe_name.should =~ encoded
  426. end
  427. end
  428. end
  429. it "should return UTF-8 output string" do
  430. name = '죾쒃쌼싁씜봜ㅛ짘홒녿'
  431. encoded = Fabricate.build(:repo, :name => name).filesystem_safe_name
  432. encoded.encoding.should == Encoding.find('UTF-8')
  433. encoded.should == name
  434. end
  435. end
  436. context "repo_store_root" do
  437. it "should return a Pathname if the path is valid" do
  438. path = '/no/such/repo-store'
  439. Razor.config.stub(:[]).with('repo_store_root').and_return(path)
  440. root = Fabricate.build(:repo, :name => "foo").repo_store_root
  441. root.should be_an_instance_of Pathname
  442. root.should == Pathname(path)
  443. end
  444. end
  445. context "unpack_repo" do
  446. let :tiny_iso do
  447. (Pathname(__FILE__).dirname.parent + 'fixtures' + 'iso' + 'tiny.iso').to_s
  448. end
  449. let :repo do
  450. Fabricate(:repo, :iso_url => "file://#{tiny_iso}")
  451. end
  452. let :command do Fabricate(:command) end
  453. it "should create the repo store root directory if absent" do
  454. Dir.mktmpdir do |tmpdir|
  455. root = Pathname(tmpdir) + 'repo-store'
  456. Razor.config['repo_store_root'] = root.to_s
  457. root.should_not exist
  458. repo.unpack_repo(command, tiny_iso)
  459. root.should exist
  460. end
  461. end
  462. it "should unpack the repo into the filesystem_safe_name under root" do
  463. Dir.mktmpdir do |root|
  464. root = Pathname(root)
  465. Razor.config['repo_store_root'] = root
  466. repo.unpack_repo(command, tiny_iso)
  467. (root + repo.filesystem_safe_name).should exist
  468. (root + repo.filesystem_safe_name + 'content.txt').should exist
  469. (root + repo.filesystem_safe_name + 'file-with-filename-that-is-longer-than-64-characters-which-some-unpackers-get-wrong.txt').should exist
  470. end
  471. end
  472. it "should unpack successfully with a unicode name" do
  473. repo.set(:name => '죾쒃쌼싁씜봜ㅛ짘홒녿').save
  474. Dir.mktmpdir do |root|
  475. root = Pathname(root)
  476. Razor.config['repo_store_root'] = root
  477. repo.unpack_repo(command, tiny_iso)
  478. (root + repo.filesystem_safe_name).should exist
  479. (root + repo.filesystem_safe_name + 'content.txt').should exist
  480. (root + repo.filesystem_safe_name + 'file-with-filename-that-is-longer-than-64-characters-which-some-unpackers-get-wrong.txt').should exist
  481. end
  482. end
  483. it "should publish 'release_temporary_repo' when unpacking completes" do
  484. expect {
  485. Dir.mktmpdir do |root|
  486. root = Pathname(root)
  487. Razor.config['repo_store_root'] = root
  488. repo.unpack_repo(command, tiny_iso)
  489. end
  490. }.to have_published(
  491. 'class' => repo.class.name,
  492. 'instance' => repo.pk_hash,
  493. 'command' => { :id => command.id },
  494. 'message' => 'release_temporary_repo'
  495. ).on(queue)
  496. end
  497. end
  498. context "release_temporary_repo" do
  499. let :repo do Fabricate(:repo) end
  500. let :command do Fabricate(:command) end
  501. it "should do nothing, successfully, if tmpdir is nil" do
  502. repo.tmpdir.should be_nil
  503. repo.release_temporary_repo(command)
  504. command.reload
  505. command.status.should == 'finished'
  506. end
  507. it "should remove the temporary directory" do
  508. Dir.mktmpdir do |tmpdir|
  509. root = Pathname(tmpdir) + 'repo-root'
  510. root.mkpath
  511. root.should exist
  512. repo.tmpdir = root
  513. repo.save
  514. repo.release_temporary_repo(command)
  515. root.should_not exist
  516. end
  517. end
  518. it "should raise an exception if removing the temporary directory fails" do
  519. # Testing with a scratch directory means that we can't, eg, discover
  520. # that someone ran the tests as root and was able to delete the
  521. # wrong thing. Much, much better safe than sorry in this case!
  522. Dir.mktmpdir do |tmpdir|
  523. tmpdir = Pathname(tmpdir)
  524. repo.tmpdir = tmpdir + 'no-such-directory'
  525. repo.save
  526. expect {
  527. repo.release_temporary_repo(command)
  528. }.to raise_error Errno::ENOENT, /no-such-directory/
  529. end
  530. end
  531. end
  532. end