cms_scanner 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +6 -0
  5. data/.travis.yml +14 -0
  6. data/Gemfile +6 -0
  7. data/README.md +20 -0
  8. data/Rakefile +9 -0
  9. data/app/app.rb +4 -0
  10. data/app/controllers.rb +2 -0
  11. data/app/controllers/core.rb +46 -0
  12. data/app/controllers/core/cli_options.rb +68 -0
  13. data/app/controllers/interesting_files.rb +12 -0
  14. data/app/finders.rb +1 -0
  15. data/app/finders/interesting_files.rb +21 -0
  16. data/app/finders/interesting_files/fantastico_fileslist.rb +23 -0
  17. data/app/finders/interesting_files/headers.rb +15 -0
  18. data/app/finders/interesting_files/robots_txt.rb +22 -0
  19. data/app/finders/interesting_files/search_replace_db_2.rb +28 -0
  20. data/app/finders/interesting_files/xml_rpc.rb +62 -0
  21. data/app/formatters.rb +3 -0
  22. data/app/formatters/cli.rb +18 -0
  23. data/app/formatters/cli_no_colour.rb +15 -0
  24. data/app/formatters/json.rb +12 -0
  25. data/app/models.rb +5 -0
  26. data/app/models/fantastico_fileslist.rb +20 -0
  27. data/app/models/headers.rb +37 -0
  28. data/app/models/interesting_file.rb +30 -0
  29. data/app/models/robots_txt.rb +20 -0
  30. data/app/models/xml_rpc.rb +35 -0
  31. data/app/views/cli/core/finished.erb +4 -0
  32. data/app/views/cli/core/started.erb +3 -0
  33. data/app/views/cli/interesting_files/findings.erb +19 -0
  34. data/app/views/cli/scan_aborted.erb +4 -0
  35. data/app/views/json/core/finished.erb +3 -0
  36. data/app/views/json/core/started.erb +3 -0
  37. data/app/views/json/interesting_files/findings.erb +1 -0
  38. data/app/views/json/scan_aborted.erb +4 -0
  39. data/cms_scanner.gemspec +37 -0
  40. data/examples/views/cli/wp_custom/test.erb +1 -0
  41. data/examples/views/json/wp_custom/test.erb +1 -0
  42. data/examples/wpscan.rb +29 -0
  43. data/lib/cms_scanner.rb +71 -0
  44. data/lib/cms_scanner/browser.rb +68 -0
  45. data/lib/cms_scanner/browser/actions.rb +48 -0
  46. data/lib/cms_scanner/browser/options.rb +53 -0
  47. data/lib/cms_scanner/cache/file_store.rb +75 -0
  48. data/lib/cms_scanner/cache/typhoeus.rb +21 -0
  49. data/lib/cms_scanner/controller.rb +90 -0
  50. data/lib/cms_scanner/controllers.rb +34 -0
  51. data/lib/cms_scanner/errors/auth_errors.rb +15 -0
  52. data/lib/cms_scanner/finders.rb +5 -0
  53. data/lib/cms_scanner/finders/finder.rb +27 -0
  54. data/lib/cms_scanner/finders/finding.rb +32 -0
  55. data/lib/cms_scanner/finders/findings.rb +25 -0
  56. data/lib/cms_scanner/finders/independent_finder.rb +30 -0
  57. data/lib/cms_scanner/finders/independent_finders.rb +41 -0
  58. data/lib/cms_scanner/formatter.rb +118 -0
  59. data/lib/cms_scanner/formatter/buffer.rb +15 -0
  60. data/lib/cms_scanner/target.rb +33 -0
  61. data/lib/cms_scanner/target/platform.rb +2 -0
  62. data/lib/cms_scanner/target/platform/php.rb +39 -0
  63. data/lib/cms_scanner/target/platform/wordpress.rb +35 -0
  64. data/lib/cms_scanner/target/platform/wordpress/custom_directories.rb +62 -0
  65. data/lib/cms_scanner/target/server.rb +3 -0
  66. data/lib/cms_scanner/target/server/apache.rb +43 -0
  67. data/lib/cms_scanner/target/server/generic.rb +34 -0
  68. data/lib/cms_scanner/target/server/iis.rb +48 -0
  69. data/lib/cms_scanner/version.rb +4 -0
  70. data/lib/cms_scanner/web_site.rb +68 -0
  71. data/lib/helper.rb +24 -0
  72. data/spec/app/controllers/core_spec.rb +152 -0
  73. data/spec/app/controllers/interesting_files_spec.rb +50 -0
  74. data/spec/app/finders/interesting_files/fantastico_fileslist_spec.rb +68 -0
  75. data/spec/app/finders/interesting_files/headers_spec.rb +38 -0
  76. data/spec/app/finders/interesting_files/robots_txt_spec.rb +56 -0
  77. data/spec/app/finders/interesting_files/search_replace_db_2_spec.rb +55 -0
  78. data/spec/app/finders/interesting_files/xml_rpc_spec.rb +138 -0
  79. data/spec/app/finders/interesting_files_spec.rb +13 -0
  80. data/spec/app/formatters/cli_no_colour_spec.rb +17 -0
  81. data/spec/app/formatters/cli_spec.rb +21 -0
  82. data/spec/app/formatters/json_spec.rb +33 -0
  83. data/spec/app/models/fantastico_fileslist_spec.rb +32 -0
  84. data/spec/app/models/headers_spec.rb +52 -0
  85. data/spec/app/models/interesting_file_spec.rb +51 -0
  86. data/spec/app/models/robots_txt_spec.rb +28 -0
  87. data/spec/app/models/xml_rpc_spec.rb +47 -0
  88. data/spec/cache/.gitignore +4 -0
  89. data/spec/dummy_finders.rb +41 -0
  90. data/spec/fixtures/interesting_files/fantastico_fileslist/fantastico_fileslist.txt +12 -0
  91. data/spec/fixtures/interesting_files/file.txt +4 -0
  92. data/spec/fixtures/interesting_files/headers/interesting.txt +14 -0
  93. data/spec/fixtures/interesting_files/headers/no_interesting.txt +12 -0
  94. data/spec/fixtures/interesting_files/robots_txt/robots.txt +10 -0
  95. data/spec/fixtures/interesting_files/search_replace_db_2/searchreplacedb2.php +188 -0
  96. data/spec/fixtures/interesting_files/xml_rpc/homepage_in_scope_pingback.html +7 -0
  97. data/spec/fixtures/interesting_files/xml_rpc/homepage_out_of_scope_pingback.html +7 -0
  98. data/spec/fixtures/interesting_files/xml_rpc/xmlrpc.php +1 -0
  99. data/spec/fixtures/output.txt +0 -0
  100. data/spec/fixtures/target/platform/php/debug_log/debug.log +2 -0
  101. data/spec/fixtures/target/platform/php/fpd/wp_rss_functions.php +2 -0
  102. data/spec/fixtures/target/platform/wordpress/custom_directories/custom_w_spaces.html +10 -0
  103. data/spec/fixtures/target/platform/wordpress/custom_directories/default.html +14 -0
  104. data/spec/fixtures/target/platform/wordpress/custom_directories/https.html +12 -0
  105. data/spec/fixtures/target/platform/wordpress/detection/default.html +4 -0
  106. data/spec/fixtures/target/platform/wordpress/detection/not_wp.html +8 -0
  107. data/spec/fixtures/target/platform/wordpress/detection/wp_includes.html +3 -0
  108. data/spec/fixtures/target/server/apache/directory_listing/2.2.16.html +15 -0
  109. data/spec/fixtures/target/server/generic/server/apache/basic.txt +5 -0
  110. data/spec/fixtures/target/server/generic/server/iis/basic.txt +6 -0
  111. data/spec/fixtures/target/server/generic/server/not_detected.txt +3 -0
  112. data/spec/fixtures/target/server/iis/directory_listing/no_parent.html +3 -0
  113. data/spec/fixtures/target/server/iis/directory_listing/with_parent.html +3 -0
  114. data/spec/fixtures/views/base/ctrl/local.erb +1 -0
  115. data/spec/fixtures/views/base/ctrl/test.erb +3 -0
  116. data/spec/fixtures/views/base/global.erb +1 -0
  117. data/spec/fixtures/views/base/test.erb +2 -0
  118. data/spec/fixtures/views/based_format/test.erb +1 -0
  119. data/spec/fixtures/views/json/render_me.erb +4 -0
  120. data/spec/lib/browser_spec.rb +141 -0
  121. data/spec/lib/cache/file_store_spec.rb +101 -0
  122. data/spec/lib/cache/typhoeus_spec.rb +30 -0
  123. data/spec/lib/cms_scanner_spec.rb +45 -0
  124. data/spec/lib/controller_spec.rb +23 -0
  125. data/spec/lib/controllers_spec.rb +52 -0
  126. data/spec/lib/finders/findings_spec.rb +49 -0
  127. data/spec/lib/finders/independent_finders_spec.rb +98 -0
  128. data/spec/lib/formatter_spec.rb +136 -0
  129. data/spec/lib/sub_scanner_spec.rb +27 -0
  130. data/spec/lib/target/platforms_spec.rb +13 -0
  131. data/spec/lib/target/servers_spec.rb +13 -0
  132. data/spec/lib/target_spec.rb +50 -0
  133. data/spec/lib/web_site_spec.rb +124 -0
  134. data/spec/shared_examples.rb +11 -0
  135. data/spec/shared_examples/browser_actions.rb +32 -0
  136. data/spec/shared_examples/finding.rb +20 -0
  137. data/spec/shared_examples/formatter_buffer.rb +8 -0
  138. data/spec/shared_examples/formatter_class_methods.rb +26 -0
  139. data/spec/shared_examples/independent_finder.rb +33 -0
  140. data/spec/shared_examples/target/platform/php.rb +58 -0
  141. data/spec/shared_examples/target/platform/wordpress.rb +41 -0
  142. data/spec/shared_examples/target/platform/wordpress/custom_directories.rb +50 -0
  143. data/spec/shared_examples/target/server/apache.rb +33 -0
  144. data/spec/shared_examples/target/server/generic.rb +34 -0
  145. data/spec/shared_examples/target/server/iis.rb +38 -0
  146. data/spec/spec_helper.rb +41 -0
  147. metadata +432 -0
@@ -0,0 +1,5 @@
1
+ Date: Sun, 12 Oct 2014 19:44:42 GMT
2
+ Server: Apache/2.2.16 (Debian)
3
+ X-Powered-By: PHP/5.3.3-7+squeeze19
4
+ Vary: Accept-Encoding
5
+ Content-Type: text/html
@@ -0,0 +1,6 @@
1
+ Content-Length: 1027
2
+ Content-Type: text/html; charset=UTF-8
3
+ Server: Microsoft-IIS/7.5
4
+ X-Powered-By: ASP.NET
5
+ X-UA-Compatible: IE=EmulateIE7
6
+ Date: Sun, 12 Oct 2014 20:15:14 GMT
@@ -0,0 +1,3 @@
1
+ Date: Sun, 12 Oct 2014 19:44:42 GMT
2
+ Vary: Accept-Encoding
3
+ Content-Type: text/html
@@ -0,0 +1,3 @@
1
+ <html><head><title>ex.lo - /dir/</title></head><body><H1>ex.lo - /dir/</H1><hr>
2
+
3
+ <pre>10/8/2014 11:00 PM &lt;dir&gt; <A HREF="/sub-dir/">sub-dir</A>10/10/2014 10:00 PM 168 <A HREF="/web.config">web.config</A><br></pre><hr></body></html>
@@ -0,0 +1,3 @@
1
+ <html><head><title>ex.lo - /dir/</title></head><body><H1>ex.lo - /dir/</H1><hr>
2
+
3
+ <pre><A HREF="/">[To Parent Directory]</A><br><br> 10/8/2014 11:00 PM &lt;dir&gt; <A HREF="/sub-dir/">sub-dir</A>10/10/2014 10:00 PM 168 <A HREF="/web.config">web.config</A><br></pre><hr></body></html>
@@ -0,0 +1 @@
1
+ Local View
@@ -0,0 +1,3 @@
1
+ Test: <%= @var %>
2
+ <%= render('local') %>
3
+ <%= render('@global') %>
@@ -0,0 +1 @@
1
+ Global View
@@ -0,0 +1,2 @@
1
+ It <%= @test %>
2
+ Views Dirs: <%= @views_directories %>
@@ -0,0 +1 @@
1
+ Override the base/test.erb
@@ -0,0 +1,4 @@
1
+ "test": <%= @test.to_json %>,
2
+ <% if @var %>
3
+ "var": <%= @var.to_json %>
4
+ <% end %>
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMSScanner::Browser do
4
+
5
+ it_behaves_like described_class::Actions
6
+
7
+ subject(:browser) { described_class.instance(options) }
8
+ before { described_class.reset }
9
+ let(:options) { {} }
10
+ let(:default) do
11
+ {
12
+ ssl_verifypeer: false, ssl_verifyhost: 2,
13
+ headers: { 'User-Agent' => "CMSScanner v#{CMSScanner::VERSION}" }
14
+ }
15
+ end
16
+
17
+ describe '#forge_request' do
18
+ it 'returns a Typhoeus::Request' do
19
+ expect(browser.forge_request('http://example.com')).to be_a Typhoeus::Request
20
+ end
21
+ end
22
+
23
+ describe '#default_request_params' do
24
+ its(:default_request_params) { should eq default }
25
+
26
+ context 'when some attributes are set' do
27
+ let(:options) do
28
+ {
29
+ cache_ttl: 200, connect_timeout: 10,
30
+ http_auth: { username: 'log', password: 'pwd' },
31
+ cookie_jar: '/tmp/cookie_jar.txt'
32
+ }
33
+ end
34
+
35
+ let(:expected) do
36
+ default.merge(
37
+ cache_ttl: 200, connecttimeout: 10, userpwd: 'log:pwd',
38
+ cookiejar: options[:cookie_jar], cookiefile: options[:cookie_jar]
39
+ )
40
+ end
41
+
42
+ its(:default_request_params) { should eq expected }
43
+ end
44
+ end
45
+
46
+ describe '#request_params' do
47
+ context 'when no param is given' do
48
+ its(:request_params) { should eq default }
49
+ end
50
+
51
+ context 'when params are supplied' do
52
+ let(:params) { { another_param: true, headers: { 'Accept' => 'None' } } }
53
+
54
+ it 'merges them (headers should be correctly merged)' do
55
+ expect(browser.request_params(params)).to eq default
56
+ .merge(params) { |key, oldval, newval| key == :headers ? oldval.merge(newval) : newval }
57
+ end
58
+
59
+ context 'when browser options' do
60
+ let(:options) { { proxy: 'http://127.0.0.1:8080' } }
61
+
62
+ it 'returns the correct hash' do
63
+ expect(browser.request_params(params)).to eq default
64
+ .merge(options)
65
+ .merge(params) { |key, oldval, newval| key == :headers ? oldval.merge(newval) : newval }
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#load_options' do
72
+ context 'when no options' do
73
+ it 'does not load anything' do
74
+ described_class::OPTIONS.each do |sym|
75
+ expected = sym == :user_agent ? "CMSScanner v#{CMSScanner::VERSION}" : nil
76
+
77
+ expect(browser.send(sym)).to eq expected
78
+ end
79
+ end
80
+ end
81
+
82
+ context 'when options are supplied' do
83
+ module CMSScanner
84
+ # Test accessor
85
+ class Browser
86
+ attr_accessor :test
87
+ end
88
+ end
89
+
90
+ let(:options) do
91
+ { cache_ttl: 200, max_threads: 10, test: 'should not be set',
92
+ user_agent: 'UA', proxy: false }
93
+ end
94
+
95
+ it 'merges the browser options only' do
96
+ described_class::OPTIONS.each do |sym|
97
+ expected = options.key?(sym) ? options[sym] : nil
98
+
99
+ expect(browser.send(sym)).to eq expected
100
+ end
101
+
102
+ expect(browser.test).to be nil
103
+ end
104
+ end
105
+ end
106
+
107
+ describe '#hydra' do
108
+ context 'when #max_threads is nil' do
109
+ its('hydra.max_concurrency') { should eq 1 }
110
+ end
111
+
112
+ context 'when #max_threads' do
113
+ let(:options) { { max_threads: 20 } }
114
+
115
+ its('hydra.max_concurrency') { should eq options[:max_threads] }
116
+ end
117
+ end
118
+
119
+ describe '#max_threads=' do
120
+ after do
121
+ browser.max_threads = @threads
122
+
123
+ expect(browser.max_threads).to eq @expected
124
+ expect(browser.hydra.max_concurrency).to eq @expected
125
+ end
126
+
127
+ context 'when <= 0' do
128
+ it 'sets the @threads to 1' do
129
+ @threads = -2
130
+ @expected = 1
131
+ end
132
+ end
133
+
134
+ context 'when > 0' do
135
+ it 'sets the @threads' do
136
+ @threads = 20
137
+ @expected = @threads
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMSScanner::Cache::FileStore do
4
+
5
+ let(:cache_dir) { File.join(CACHE, 'cache_file_store') }
6
+ subject(:cache) { described_class.new(cache_dir) }
7
+
8
+ before { FileUtils.rm_r(cache_dir, secure: true) if Dir.exist?(cache_dir) }
9
+ after { cache.clean }
10
+
11
+ describe '#new, #storage_path, #serializer' do
12
+ its(:serializer) { should be Marshal }
13
+ its(:storage_path) { should eq cache_dir }
14
+ end
15
+
16
+ describe '#clean' do
17
+ it 'removes all files from the cache dir' do
18
+ # let's create some files into the directory first
19
+ (0..5).each do |i|
20
+ File.new(File.join(cache.storage_path, "file_#{i}.txt"), File::CREAT)
21
+ end
22
+
23
+ expect(count_files_in_dir(cache.storage_path, 'file_*.txt')).to eq 6
24
+ cache.clean
25
+ expect(count_files_in_dir(cache.storage_path)).to eq 0
26
+ end
27
+ end
28
+
29
+ describe '#read_entry?' do
30
+ let(:key) { 'key1' }
31
+
32
+ after do
33
+ File.write(cache.entry_expiration_path(key), @expiration) if @expiration
34
+
35
+ expect(cache.read_entry(key)).to eq @expected
36
+ end
37
+
38
+ context 'when the entry does not exists' do
39
+ it 'returns nil' do
40
+ @expected = nil
41
+ end
42
+ end
43
+
44
+ context 'when the file is empty (marshal data too short error)' do
45
+ it 'returns nil' do
46
+ File.new(cache.entry_path(key), File::CREAT)
47
+
48
+ @expiration = Time.now.to_i + 200
49
+ @expected = nil
50
+ end
51
+ end
52
+
53
+ context 'when the entry has expired' do
54
+ it 'returns nil' do
55
+ @expiration = Time.now.to_i - 200
56
+ @expected = nil
57
+ end
58
+ end
59
+
60
+ context 'when the entry has not expired' do
61
+ it 'returns the entry' do
62
+ File.write(cache.entry_path(key), cache.serializer.dump('testing data'))
63
+
64
+ @expiration = Time.now.to_i + 600
65
+ @expected = 'testing data'
66
+ end
67
+ end
68
+ end
69
+
70
+ describe '#write_entry' do
71
+ after do
72
+ cache.write_entry(@key, @data, @ttl)
73
+ expect(cache.read_entry(@key)).to eq @expected
74
+ end
75
+
76
+ it 'should get the correct entry (string)' do
77
+ @ttl = 10
78
+ @key = 'some_key'
79
+ @data = 'Hello World !'
80
+ @expected = @data
81
+ end
82
+
83
+ context 'when cache_ttl <= 0' do
84
+ it 'does not write the entry' do
85
+ @ttl = 0
86
+ @key = 'another_key'
87
+ @data = 'Another Hello World !'
88
+ @expected = nil
89
+ end
90
+ end
91
+
92
+ context 'when cache_ttl is nil' do
93
+ it 'does not write the entry' do
94
+ @ttl = nil
95
+ @key = 'test'
96
+ @data = 'test'
97
+ @expected = nil
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMSScanner::Cache::Typhoeus do
4
+
5
+ subject(:cache) { described_class.new(cache_dir) }
6
+
7
+ let(:cache_dir) { File.join(CACHE, 'typhoeus_cache') }
8
+ let(:url) { 'http://example.com' }
9
+ let(:request) { Typhoeus::Request.new(url, cache_ttl: 20) }
10
+ let(:key) { request.hash.to_s }
11
+
12
+ describe '#get' do
13
+ it 'calls #read_entry' do
14
+ expect(cache).to receive(:read_entry).with(key)
15
+
16
+ cache.get(request)
17
+ end
18
+ end
19
+
20
+ describe '#set' do
21
+ let(:response) { Typhoeus::Response.new }
22
+
23
+ it 'calls #write_entry' do
24
+ expect(cache).to receive(:write_entry).with(key, response, request.cache_ttl)
25
+
26
+ cache.set(request, response)
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ module CMSScanner
4
+ module Controller
5
+ # Failure class for testing
6
+ class SpecFailure < Base
7
+ def before_scan
8
+ fail 'error spotted'
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ describe CMSScanner::Scan do
15
+
16
+ subject(:scanner) { described_class.new }
17
+ let(:controller) { CMSScanner::Controller }
18
+
19
+ describe '#new, #controllers' do
20
+ its(:controllers) { should eq([controller::Core.new]) }
21
+ end
22
+
23
+ describe '#run' do
24
+ it 'runs the controlllers and calls the formatter#beautify' do
25
+ expect(scanner.controllers).to receive(:run)
26
+ expect(scanner.formatter).to receive(:beautify)
27
+ scanner.run
28
+ end
29
+
30
+ context 'when an error is raised during the #run' do
31
+ it 'aborts the scan with the associated output' do
32
+ scanner.controllers[0] = controller::SpecFailure.new
33
+
34
+ expect(scanner.formatter).to receive(:output)
35
+ .with('@scan_aborted', hash_including(:reason, :trace, :verbose))
36
+
37
+ scanner.run
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '#datastore' do
43
+ its(:datastore) { should eq({}) }
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMSScanner::Controller do
4
+
5
+ subject(:controller) { described_class::Base.new }
6
+
7
+ context 'when parsed_options' do
8
+ before { described_class::Base.parsed_options = parsed_options }
9
+ let(:parsed_options) { { url: 'http://example.com/' } }
10
+
11
+ its(:parsed_options) { should eq(parsed_options) }
12
+ its(:formatter) { should be_a CMSScanner::Formatter::Cli }
13
+ its(:target) { should be_a CMSScanner::Target }
14
+
15
+ describe '#render' do
16
+ it 'calls the formatter#render' do
17
+ expect(controller.formatter).to receive(:render).with('test', { verbose: nil }, 'base')
18
+ controller.render('test')
19
+ end
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ module CMSScanner
4
+ module Controller
5
+ class Spec < Base
6
+ end
7
+ end
8
+ end
9
+
10
+ describe CMSScanner::Controllers do
11
+
12
+ subject(:controllers) { described_class.new }
13
+ let(:controller_mod) { CMSScanner::Controller }
14
+
15
+ describe '#<<' do
16
+ its(:size) { should be 0 }
17
+
18
+ context 'when controllers are added' do
19
+ before { controllers << controller_mod::Spec.new << controller_mod::Base.new }
20
+
21
+ its(:size) { should be 2 }
22
+ end
23
+
24
+ context 'when a controller is added twice' do
25
+ before { 2.times { controllers << controller_mod::Spec.new } }
26
+
27
+ its(:size) { should be 1 }
28
+ end
29
+
30
+ it 'returns self' do
31
+ expect(controllers << controller_mod::Spec.new).to be_a described_class
32
+ end
33
+ end
34
+
35
+ describe '#run' do
36
+ it 'runs the before_scan, run and after_scan methods of each controller' do
37
+ spec = controller_mod::Spec.new
38
+ base = controller_mod::Base.new
39
+
40
+ controllers << spec << base
41
+
42
+ # TODO: Any way to test the orders ? (after_scan should reverse the order)
43
+ [base, spec].each do |c|
44
+ expect(c).to receive(:before_scan)
45
+ expect(c).to receive(:run)
46
+ expect(c).to receive(:after_scan)
47
+ end
48
+ controllers.run
49
+ end
50
+ end
51
+
52
+ end