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