Merge branch '13365-note-bug-in-docs' refs #13365
[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).each do |name, ob|
89           ob.reject! { |k, v| k.start_with?('secret_') }
90         end
91       end
92       keys.inject(@@api_fixtures[name]) { |hash, key| hash[key] }.deep_dup
93     end
94   end
95   def api_fixture(name, *keys)
96     self.class.api_fixture(name, *keys)
97   end
98
99   def find_fixture(object_class, name)
100     object_class.find(api_fixture(object_class.to_s.pluralize.underscore,
101                                   name, "uuid"))
102   end
103 end
104
105 module ApiMockHelpers
106   def fake_api_response body, status_code, headers
107     resp = mock
108     resp.responds_like_instance_of HTTP::Message
109     resp.stubs(:headers).returns headers
110     resp.stubs(:content).returns body
111     resp.stubs(:status_code).returns status_code
112     resp
113   end
114
115   def stub_api_calls_with_body body, status_code=200, headers={}
116     stub_api_calls
117     resp = fake_api_response body, status_code, headers
118     stub_api_client.stubs(:post).returns resp
119   end
120
121   def stub_api_calls
122     @stubbed_client = ArvadosApiClient.new
123     @stubbed_client.instance_eval do
124       @api_client = HTTPClient.new
125     end
126     ArvadosApiClient.stubs(:new_or_current).returns(@stubbed_client)
127   end
128
129   def stub_api_calls_with_invalid_json
130     stub_api_calls_with_body ']"omg,bogus"['
131   end
132
133   # Return the HTTPClient mock used by the ArvadosApiClient mock. You
134   # must have called stub_api_calls first.
135   def stub_api_client
136     @stubbed_client.instance_eval do
137       @api_client
138     end
139   end
140 end
141
142 class ActiveSupport::TestCase
143   include ApiMockHelpers
144 end
145
146 class ActiveSupport::TestCase
147   include ApiFixtureLoader
148   def session_for api_client_auth_name
149     {
150       arvados_api_token: api_fixture('api_client_authorizations')[api_client_auth_name.to_s]['api_token']
151     }
152   end
153   def json_response
154     Oj.load(@response.body)
155   end
156 end
157
158 class ApiServerForTests
159   PYTHON_TESTS_DIR = File.expand_path('../../../../sdk/python/tests', __FILE__)
160   ARV_API_SERVER_DIR = File.expand_path('../../../../services/api', __FILE__)
161   SERVER_PID_PATH = File.expand_path('tmp/pids/test-server.pid', ARV_API_SERVER_DIR)
162   WEBSOCKET_PID_PATH = File.expand_path('tmp/pids/test-server.pid', ARV_API_SERVER_DIR)
163   @main_process_pid = $$
164   @@server_is_running = false
165
166   def check_output *args
167     output = nil
168     Bundler.with_clean_env do
169       output = IO.popen *args do |io|
170         io.read
171       end
172       if not $?.success?
173         raise RuntimeError, "Command failed (#{$?}): #{args.inspect}"
174       end
175     end
176     output
177   end
178
179   def run_test_server
180     env_script = nil
181     Dir.chdir PYTHON_TESTS_DIR do
182       # These are no-ops if we're running within run-tests.sh (except
183       # that we do get a useful env_script back from "start", even
184       # though it doesn't need to start up a new server).
185       env_script = check_output %w(python ./run_test_server.py start --auth admin)
186       check_output %w(python ./run_test_server.py start_arv-git-httpd)
187       check_output %w(python ./run_test_server.py start_keep-web)
188       check_output %w(python ./run_test_server.py start_nginx)
189       # This one isn't a no-op, even under run-tests.sh.
190       check_output %w(python ./run_test_server.py start_keep)
191     end
192     test_env = {}
193     env_script.each_line do |line|
194       line = line.chomp
195       if 0 == line.index('export ')
196         toks = line.sub('export ', '').split '=', 2
197         $stderr.puts "run_test_server.py: #{toks[0]}=#{toks[1]}"
198         test_env[toks[0]] = toks[1]
199       end
200     end
201     test_env
202   end
203
204   def stop_test_server
205     Dir.chdir PYTHON_TESTS_DIR do
206       check_output %w(python ./run_test_server.py stop_keep)
207       # These are no-ops if we're running within run-tests.sh
208       check_output %w(python ./run_test_server.py stop_nginx)
209       check_output %w(python ./run_test_server.py stop_arv-git-httpd)
210       check_output %w(python ./run_test_server.py stop_keep-web)
211       check_output %w(python ./run_test_server.py stop)
212     end
213     @@server_is_running = false
214   end
215
216   def run args=[]
217     return if @@server_is_running
218
219     # Stop server left over from interrupted previous run
220     stop_test_server
221
222     ::MiniTest.after_run do
223       stop_test_server
224     end
225
226     test_env = run_test_server
227     $application_config['arvados_login_base'] = "https://#{test_env['ARVADOS_API_HOST']}/login"
228     $application_config['arvados_v1_base'] = "https://#{test_env['ARVADOS_API_HOST']}/arvados/v1"
229     $application_config['arvados_insecure_host'] = true
230     ActiveSupport::TestCase.reset_application_config
231
232     @@server_is_running = true
233   end
234
235   def run_rake_task task_name, arg_string
236     Dir.chdir ARV_API_SERVER_DIR do
237       check_output ['bundle', 'exec', 'rake', "#{task_name}[#{arg_string}]"]
238     end
239   end
240 end
241
242 class ActionController::TestCase
243   setup do
244     @test_counter = 0
245   end
246
247   def check_counter action
248     @test_counter += 1
249     if @test_counter == 2
250       assert_equal 1, 2, "Multiple actions in controller test"
251     end
252   end
253
254   [:get, :post, :put, :patch, :delete].each do |method|
255     define_method method do |action, *args|
256       check_counter action
257       super action, *args
258     end
259   end
260 end
261
262 # Test classes can call reset_api_fixtures(when_to_reset,flag) to
263 # override the default. Example:
264 #
265 # class MySuite < ActionDispatch::IntegrationTest
266 #   reset_api_fixtures :after_each_test, false
267 #   reset_api_fixtures :after_suite, true
268 #   ...
269 # end
270 #
271 # The default behavior is reset_api_fixtures(:after_each_test,true).
272 #
273 class ActiveSupport::TestCase
274
275   def self.inherited subclass
276     subclass.class_eval do
277       class << self
278         attr_accessor :want_reset_api_fixtures
279       end
280       @want_reset_api_fixtures = {
281         after_each_test: true,
282         after_suite: false,
283         before_suite: false,
284       }
285     end
286     super
287   end
288   # Existing subclasses of ActiveSupport::TestCase (ones that already
289   # existed before we set up the self.inherited hook above) will not
290   # get their own instance variable. They're not real test cases
291   # anyway, so we give them a "don't reset anywhere" stub.
292   def self.want_reset_api_fixtures
293     {}
294   end
295
296   def self.reset_api_fixtures where, t=true
297     if not want_reset_api_fixtures.has_key? where
298       raise ArgumentError, "There is no #{where.inspect} hook"
299     end
300     self.want_reset_api_fixtures[where] = t
301   end
302
303   def self.run *args
304     reset_api_fixtures_now if want_reset_api_fixtures[:before_suite]
305     result = super
306     reset_api_fixtures_now if want_reset_api_fixtures[:after_suite]
307     result
308   end
309
310   def after_teardown
311     if self.class.want_reset_api_fixtures[:after_each_test] and
312         @want_reset_api_fixtures != false
313       self.class.reset_api_fixtures_now
314     end
315     super
316   end
317
318   def reset_api_fixtures_after_test t=true
319     @want_reset_api_fixtures = t
320   end
321
322   protected
323   def self.reset_api_fixtures_now
324     # Never try to reset fixtures when we're just using test
325     # infrastructure to run performance/diagnostics suites.
326     return unless Rails.env == 'test'
327
328     auth = api_fixture('api_client_authorizations')['admin_trustedclient']
329     Thread.current[:arvados_api_token] = auth['api_token']
330     ArvadosApiClient.new.api(nil, '../../database/reset', {})
331     Thread.current[:arvados_api_token] = nil
332   end
333 end
334
335 # If it quacks like a duck, it must be a HTTP request object.
336 class RequestDuck
337   def self.host
338     "localhost"
339   end
340
341   def self.port
342     8080
343   end
344
345   def self.protocol
346     "http"
347   end
348 end
349
350 # Example:
351 #
352 # apps/workbench$ RAILS_ENV=test bundle exec irb -Ilib:test
353 # > load 'test/test_helper.rb'
354 # > singletest 'integration/collection_upload_test.rb', 'Upload two empty files'
355 #
356 def singletest test_class_file, test_name
357   load File.join('test', test_class_file)
358   Minitest.run ['-v', '-n', "test_#{test_name.gsub ' ', '_'}"]
359   Object.send(:remove_const,
360               test_class_file.gsub(/.*\/|\.rb$/, '').camelize.to_sym)
361   ::Minitest::Runnable.runnables.reject! { true }
362 end
363
364 if ENV["RAILS_ENV"].eql? 'test'
365   ApiServerForTests.new.run
366   ApiServerForTests.new.run ["--websockets"]
367 end
368
369 # Reset fixtures now (i.e., before any tests run).
370 ActiveSupport::TestCase.reset_api_fixtures_now