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