Merge branch '8784-dir-listings'
[arvados.git] / apps / workbench / test / test_helper.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 ENV["RAILS_ENV"] = "test" if (ENV["RAILS_ENV"] != "diagnostics" and ENV["RAILS_ENV"] != "performance")
6
7 unless ENV["NO_COVERAGE_TEST"]
8   begin
9     require 'simplecov'
10     require 'simplecov-rcov'
11     class SimpleCov::Formatter::MergedFormatter
12       def format(result)
13         SimpleCov::Formatter::HTMLFormatter.new.format(result)
14         SimpleCov::Formatter::RcovFormatter.new.format(result)
15       end
16     end
17     SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
18     SimpleCov.start do
19       add_filter '/test/'
20       add_filter 'initializers/secret_token'
21     end
22   rescue Exception => e
23     $stderr.puts "SimpleCov unavailable (#{e}). Proceeding without."
24   end
25 end
26
27 require File.expand_path('../../config/environment', __FILE__)
28 require 'rails/test_help'
29 require 'mocha/mini_test'
30
31 class ActiveSupport::TestCase
32   # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in
33   # alphabetical order.
34   #
35   # Note: You'll currently still have to declare fixtures explicitly
36   # in integration tests -- they do not yet inherit this setting
37   fixtures :all
38   def use_token(token_name)
39     user_was = Thread.current[:user]
40     token_was = Thread.current[:arvados_api_token]
41     auth = api_fixture('api_client_authorizations')[token_name.to_s]
42     Thread.current[:arvados_api_token] = auth['api_token']
43     if block_given?
44       begin
45         yield
46       ensure
47         Thread.current[:user] = user_was
48         Thread.current[:arvados_api_token] = token_was
49       end
50     end
51   end
52
53   teardown do
54     Thread.current[:arvados_api_token] = nil
55     Thread.current[:user] = nil
56     Thread.current[:reader_tokens] = nil
57     # Diagnostics suite doesn't run a server, so there's no cache to clear.
58     Rails.cache.clear unless (Rails.env == "diagnostics")
59     # Restore configuration settings changed during tests
60     self.class.reset_application_config
61   end
62
63   def self.reset_application_config
64     $application_config.each do |k,v|
65       if k.match /^[^.]*$/
66         Rails.configuration.send (k + '='), v
67       end
68     end
69   end
70 end
71
72 module ApiFixtureLoader
73   def self.included(base)
74     base.extend(ClassMethods)
75   end
76
77   module ClassMethods
78     @@api_fixtures = {}
79     def api_fixture(name, *keys)
80       # Returns the data structure from the named API server test fixture.
81       @@api_fixtures[name] ||= \
82       begin
83         path = File.join(ApiServerForTests::ARV_API_SERVER_DIR,
84                          'test', 'fixtures', "#{name}.yml")
85         file = IO.read(path)
86         trim_index = file.index('# Test Helper trims the rest of the file')
87         file = file[0, trim_index] if trim_index
88         YAML.load(file)
89       end
90       keys.inject(@@api_fixtures[name]) { |hash, key| hash[key].deep_dup }
91     end
92   end
93   def api_fixture(name, *keys)
94     self.class.api_fixture(name, *keys)
95   end
96
97   def find_fixture(object_class, name)
98     object_class.find(api_fixture(object_class.to_s.pluralize.underscore,
99                                   name, "uuid"))
100   end
101 end
102
103 module ApiMockHelpers
104   def fake_api_response body, status_code, headers
105     resp = mock
106     resp.responds_like_instance_of HTTP::Message
107     resp.stubs(:headers).returns headers
108     resp.stubs(:content).returns body
109     resp.stubs(:status_code).returns status_code
110     resp
111   end
112
113   def stub_api_calls_with_body body, status_code=200, headers={}
114     stub_api_calls
115     resp = fake_api_response body, status_code, headers
116     stub_api_client.stubs(:post).returns resp
117   end
118
119   def stub_api_calls
120     @stubbed_client = ArvadosApiClient.new
121     @stubbed_client.instance_eval do
122       @api_client = HTTPClient.new
123     end
124     ArvadosApiClient.stubs(:new_or_current).returns(@stubbed_client)
125   end
126
127   def stub_api_calls_with_invalid_json
128     stub_api_calls_with_body ']"omg,bogus"['
129   end
130
131   # Return the HTTPClient mock used by the ArvadosApiClient mock. You
132   # must have called stub_api_calls first.
133   def stub_api_client
134     @stubbed_client.instance_eval do
135       @api_client
136     end
137   end
138 end
139
140 class ActiveSupport::TestCase
141   include ApiMockHelpers
142 end
143
144 class ActiveSupport::TestCase
145   include ApiFixtureLoader
146   def session_for api_client_auth_name
147     {
148       arvados_api_token: api_fixture('api_client_authorizations')[api_client_auth_name.to_s]['api_token']
149     }
150   end
151   def json_response
152     Oj.load(@response.body)
153   end
154 end
155
156 class ApiServerForTests
157   PYTHON_TESTS_DIR = File.expand_path('../../../../sdk/python/tests', __FILE__)
158   ARV_API_SERVER_DIR = File.expand_path('../../../../services/api', __FILE__)
159   SERVER_PID_PATH = File.expand_path('tmp/pids/test-server.pid', ARV_API_SERVER_DIR)
160   WEBSOCKET_PID_PATH = File.expand_path('tmp/pids/test-server.pid', ARV_API_SERVER_DIR)
161   @main_process_pid = $$
162   @@server_is_running = false
163
164   def check_output *args
165     output = nil
166     Bundler.with_clean_env do
167       output = IO.popen *args do |io|
168         io.read
169       end
170       if not $?.success?
171         raise RuntimeError, "Command failed (#{$?}): #{args.inspect}"
172       end
173     end
174     output
175   end
176
177   def run_test_server
178     env_script = nil
179     Dir.chdir PYTHON_TESTS_DIR do
180       # These are no-ops if we're running within run-tests.sh (except
181       # that we do get a useful env_script back from "start", even
182       # though it doesn't need to start up a new server).
183       env_script = check_output %w(python ./run_test_server.py start --auth admin)
184       check_output %w(python ./run_test_server.py start_arv-git-httpd)
185       check_output %w(python ./run_test_server.py start_keep-web)
186       check_output %w(python ./run_test_server.py start_nginx)
187       # This one isn't a no-op, even under run-tests.sh.
188       check_output %w(python ./run_test_server.py start_keep)
189     end
190     test_env = {}
191     env_script.each_line do |line|
192       line = line.chomp
193       if 0 == line.index('export ')
194         toks = line.sub('export ', '').split '=', 2
195         $stderr.puts "run_test_server.py: #{toks[0]}=#{toks[1]}"
196         test_env[toks[0]] = toks[1]
197       end
198     end
199     test_env
200   end
201
202   def stop_test_server
203     Dir.chdir PYTHON_TESTS_DIR do
204       check_output %w(python ./run_test_server.py stop_keep)
205       # These are no-ops if we're running within run-tests.sh
206       check_output %w(python ./run_test_server.py stop_nginx)
207       check_output %w(python ./run_test_server.py stop_arv-git-httpd)
208       check_output %w(python ./run_test_server.py stop_keep-web)
209       check_output %w(python ./run_test_server.py stop)
210     end
211     @@server_is_running = false
212   end
213
214   def run args=[]
215     return if @@server_is_running
216
217     # Stop server left over from interrupted previous run
218     stop_test_server
219
220     ::MiniTest.after_run do
221       stop_test_server
222     end
223
224     test_env = run_test_server
225     $application_config['arvados_login_base'] = "https://#{test_env['ARVADOS_API_HOST']}/login"
226     $application_config['arvados_v1_base'] = "https://#{test_env['ARVADOS_API_HOST']}/arvados/v1"
227     $application_config['arvados_insecure_host'] = true
228     ActiveSupport::TestCase.reset_application_config
229
230     @@server_is_running = true
231   end
232
233   def run_rake_task task_name, arg_string
234     Dir.chdir ARV_API_SERVER_DIR do
235       check_output ['bundle', 'exec', 'rake', "#{task_name}[#{arg_string}]"]
236     end
237   end
238 end
239
240 class ActionController::TestCase
241   setup do
242     @test_counter = 0
243   end
244
245   def check_counter action
246     @test_counter += 1
247     if @test_counter == 2
248       assert_equal 1, 2, "Multiple actions in controller test"
249     end
250   end
251
252   [:get, :post, :put, :patch, :delete].each do |method|
253     define_method method do |action, *args|
254       check_counter action
255       super action, *args
256     end
257   end
258 end
259
260 # Test classes can call reset_api_fixtures(when_to_reset,flag) to
261 # override the default. Example:
262 #
263 # class MySuite < ActionDispatch::IntegrationTest
264 #   reset_api_fixtures :after_each_test, false
265 #   reset_api_fixtures :after_suite, true
266 #   ...
267 # end
268 #
269 # The default behavior is reset_api_fixtures(:after_each_test,true).
270 #
271 class ActiveSupport::TestCase
272
273   def self.inherited subclass
274     subclass.class_eval do
275       class << self
276         attr_accessor :want_reset_api_fixtures
277       end
278       @want_reset_api_fixtures = {
279         after_each_test: true,
280         after_suite: false,
281         before_suite: false,
282       }
283     end
284     super
285   end
286   # Existing subclasses of ActiveSupport::TestCase (ones that already
287   # existed before we set up the self.inherited hook above) will not
288   # get their own instance variable. They're not real test cases
289   # anyway, so we give them a "don't reset anywhere" stub.
290   def self.want_reset_api_fixtures
291     {}
292   end
293
294   def self.reset_api_fixtures where, t=true
295     if not want_reset_api_fixtures.has_key? where
296       raise ArgumentError, "There is no #{where.inspect} hook"
297     end
298     self.want_reset_api_fixtures[where] = t
299   end
300
301   def self.run *args
302     reset_api_fixtures_now if want_reset_api_fixtures[:before_suite]
303     result = super
304     reset_api_fixtures_now if want_reset_api_fixtures[:after_suite]
305     result
306   end
307
308   def after_teardown
309     if self.class.want_reset_api_fixtures[:after_each_test] and
310         @want_reset_api_fixtures != false
311       self.class.reset_api_fixtures_now
312     end
313     super
314   end
315
316   def reset_api_fixtures_after_test t=true
317     @want_reset_api_fixtures = t
318   end
319
320   protected
321   def self.reset_api_fixtures_now
322     # Never try to reset fixtures when we're just using test
323     # infrastructure to run performance/diagnostics suites.
324     return unless Rails.env == 'test'
325
326     auth = api_fixture('api_client_authorizations')['admin_trustedclient']
327     Thread.current[:arvados_api_token] = auth['api_token']
328     ArvadosApiClient.new.api(nil, '../../database/reset', {})
329     Thread.current[:arvados_api_token] = nil
330   end
331 end
332
333 # If it quacks like a duck, it must be a HTTP request object.
334 class RequestDuck
335   def self.host
336     "localhost"
337   end
338
339   def self.port
340     8080
341   end
342
343   def self.protocol
344     "http"
345   end
346 end
347
348 # Example:
349 #
350 # apps/workbench$ RAILS_ENV=test bundle exec irb -Ilib:test
351 # > load 'test/test_helper.rb'
352 # > singletest 'integration/collection_upload_test.rb', 'Upload two empty files'
353 #
354 def singletest test_class_file, test_name
355   load File.join('test', test_class_file)
356   Minitest.run ['-v', '-n', "test_#{test_name.gsub ' ', '_'}"]
357   Object.send(:remove_const,
358               test_class_file.gsub(/.*\/|\.rb$/, '').camelize.to_sym)
359   ::Minitest::Runnable.runnables.reject! { true }
360 end
361
362 if ENV["RAILS_ENV"].eql? 'test'
363   ApiServerForTests.new.run
364   ApiServerForTests.new.run ["--websockets"]
365 end
366
367 # Reset fixtures now (i.e., before any tests run).
368 ActiveSupport::TestCase.reset_api_fixtures_now