test_helper.rb 12.6 KB
Newer Older
1
# Redmine - project management software
jplang's avatar
jplang committed
2
# Copyright (C) 2006-2017  Jean-Philippe Lang
jplang's avatar
v0.2.0    
jplang committed
3
4
5
6
7
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
8
#
jplang's avatar
v0.2.0    
jplang committed
9
10
11
12
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
13
#
jplang's avatar
v0.2.0    
jplang committed
14
15
16
17
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

18
19
20
21
22
23
24
if ENV["COVERAGE"]
  require 'simplecov'
  require File.expand_path(File.dirname(__FILE__) + "/coverage/html_formatter")
  SimpleCov.formatter = Redmine::Coverage::HtmlFormatter
  SimpleCov.start 'rails'
end

25
26
$redmine_test_ldap_server = ENV['REDMINE_TEST_LDAP_SERVER'] || '127.0.0.1'

27
ENV["RAILS_ENV"] = "test"
jplang's avatar
jplang committed
28
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
jplang's avatar
jplang committed
29
require 'rails/test_help'
30
require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
jplang's avatar
jplang committed
31

32
33
require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
include ObjectHelpers
34

35
require 'net/ldap'
jplang's avatar
jplang committed
36
require 'mocha/setup'
jplang's avatar
jplang committed
37
require 'fileutils'
38

39
40
Redmine::SudoMode.disable!

jplang's avatar
jplang committed
41
42
43
$redmine_tmp_attachments_directory = "#{Rails.root}/tmp/test/attachments"
FileUtils.mkdir_p $redmine_tmp_attachments_directory

jplang's avatar
jplang committed
44
45
46
47
$redmine_tmp_pdf_directory = "#{Rails.root}/tmp/test/pdf"
FileUtils.mkdir_p $redmine_tmp_pdf_directory
FileUtils.rm Dir.glob('#$redmine_tmp_pdf_directory/*.pdf')

jplang's avatar
jplang committed
48
49
50
51
52
class ActionView::TestCase
  helper :application
  include ApplicationHelper
end

edavis10's avatar
edavis10 committed
53
class ActiveSupport::TestCase
jplang's avatar
jplang committed
54
  include ActionDispatch::TestProcess
jplang's avatar
jplang committed
55

jplang's avatar
jplang committed
56
  self.use_transactional_tests = true
jplang's avatar
jplang committed
57
58
  self.use_instantiated_fixtures  = false

jplang's avatar
jplang committed
59
  def uploaded_test_file(name, mime)
jplang's avatar
jplang committed
60
    fixture_file_upload("files/#{name}", mime, true)
61
  end
edavis10's avatar
edavis10 committed
62

jplang's avatar
jplang committed
63
64
65
66
67
68
  def mock_file(options=nil)
    options ||= {
        :original_filename => 'a_file.png',
        :content_type => 'image/png',
        :size => 32
      }
edavis10's avatar
edavis10 committed
69

jplang's avatar
jplang committed
70
    Redmine::MockFile.new(options)
edavis10's avatar
edavis10 committed
71
72
  end

73
  def mock_file_with_options(options={})
jplang's avatar
jplang committed
74
    mock_file(options)
75
76
  end

jplang's avatar
jplang committed
77
78
  # Use a temporary directory for attachment related tests
  def set_tmp_attachments_directory
jplang's avatar
jplang committed
79
    Attachment.storage_path = $redmine_tmp_attachments_directory
jplang's avatar
jplang committed
80
  end
81

82
83
84
85
  def set_fixtures_attachments_directory
    Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
  end

86
  def with_settings(options, &block)
87
88
89
90
91
92
93
94
95
    saved_settings = options.keys.inject({}) do |h, k|
      h[k] = case Setting[k]
        when Symbol, false, true, nil
          Setting[k]
        else
          Setting[k].dup
        end
      h
    end
96
97
    options.each {|k, v| Setting[k] = v}
    yield
98
  ensure
99
    saved_settings.each {|k, v| Setting[k] = v} if saved_settings
100
  end
101

102
103
104
105
106
107
108
109
110
  # Yields the block with user as the current user
  def with_current_user(user, &block)
    saved_user = User.current
    User.current = user
    yield
  ensure
    User.current = saved_user
  end

111
112
113
114
115
116
117
118
  def with_locale(locale, &block)
    saved_localed = ::I18n.locale
    ::I18n.locale = locale
    yield
  ensure
    ::I18n.locale = saved_localed
  end

119
  def self.ldap_configured?
120
    @test_ldap = Net::LDAP.new(:host => $redmine_test_ldap_server, :port => 389)
121
    return @test_ldap.bind
122
123
124
  rescue Exception => e
    # LDAP is not listening
    return nil
125
  end
126

127
  def self.convert_installed?
jplang's avatar
jplang committed
128
    Redmine::Thumbnail.convert_available?
129
130
  end

131
132
133
134
  def convert_installed?
    self.class.convert_installed?
  end

135
136
  # Returns the path to the test +vendor+ repository
  def self.repository_path(vendor)
jplang's avatar
jplang committed
137
138
139
    path = Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
    # Unlike ruby, JRuby returns Rails.root with backslashes under Windows
    path.tr("\\", "/")
140
  end
141

tmaruyama's avatar
tmaruyama committed
142
  # Returns the url of the subversion test repository
143
144
145
146
147
  def self.subversion_repository_url
    path = repository_path('subversion')
    path = '/' + path unless path.starts_with?('/')
    "file://#{path}"
  end
148

149
150
151
152
  # Returns true if the +vendor+ test repository is configured
  def self.repository_configured?(vendor)
    File.directory?(repository_path(vendor))
  end
153

154
155
156
  def repository_path_hash(arr)
    hs = {}
    hs[:path]  = arr.join("/")
jplang's avatar
jplang committed
157
    hs[:param] = arr.join("/")
158
159
160
    hs
  end

jplang's avatar
jplang committed
161
162
163
164
  def sqlite?
    ActiveRecord::Base.connection.adapter_name =~ /sqlite/i
  end

165
166
167
168
  def mysql?
    ActiveRecord::Base.connection.adapter_name =~ /mysql/i
  end

169
170
171
172
  def postgresql?
    ActiveRecord::Base.connection.adapter_name =~ /postgresql/i
  end

173
174
175
176
177
  def quoted_date(date)
    date = Date.parse(date) if date.is_a?(String)
    ActiveRecord::Base.connection.quoted_date(date)
  end

178
179
  # Asserts that a new record for the given class is created
  # and returns it
180
  def new_record(klass, &block)
jplang's avatar
jplang committed
181
182
183
184
185
186
187
    new_records(klass, 1, &block).first
  end

  # Asserts that count new records for the given class are created
  # and returns them as an array order by object id
  def new_records(klass, count, &block)
    assert_difference "#{klass}.count", count do
188
189
      yield
    end
jplang's avatar
jplang committed
190
    klass.order(:id => :desc).limit(count).to_a.reverse
191
192
  end

193
194
195
196
197
198
199
200
  def assert_save(object)
    saved = object.save
    message = "#{object.class} could not be saved"
    errors = object.errors.full_messages.map {|m| "- #{m}"}
    message << ":\n#{errors.join("\n")}" if errors.any?
    assert_equal true, saved, message
  end

201
202
  def assert_select_error(arg)
    assert_select '#errorExplanation', :text => arg
203
  end
204

jplang's avatar
jplang committed
205
206
  def assert_include(expected, s, message=nil)
    assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
207
  end
208

209
210
  def assert_not_include(expected, s, message=nil)
    assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
jplang's avatar
jplang committed
211
212
  end

213
  def assert_select_in(text, *args, &block)
jplang's avatar
jplang committed
214
    d = Nokogiri::HTML(CGI::unescapeHTML(String.new(text))).root
215
216
217
    assert_select(d, *args, &block)
  end

jplang's avatar
jplang committed
218
219
220
221
222
223
224
225
  def assert_select_email(*args, &block)
    email = ActionMailer::Base.deliveries.last
    assert_not_nil email
    html_body = email.parts.detect {|part| part.content_type.include?('text/html')}.try(&:body)
    assert_not_nil html_body
    assert_select_in html_body.encoded, *args, &block
  end

226
  def assert_mail_body_match(expected, mail, message=nil)
jplang's avatar
jplang committed
227
    if expected.is_a?(String)
228
      assert_include expected, mail_body(mail), message
jplang's avatar
jplang committed
229
    else
230
      assert_match expected, mail_body(mail), message
jplang's avatar
jplang committed
231
232
233
    end
  end

234
  def assert_mail_body_no_match(expected, mail, message=nil)
jplang's avatar
jplang committed
235
    if expected.is_a?(String)
236
      assert_not_include expected, mail_body(mail), message
jplang's avatar
jplang committed
237
    else
238
      assert_no_match expected, mail_body(mail), message
jplang's avatar
jplang committed
239
240
241
    end
  end

242
  def mail_body(mail)
jplang's avatar
jplang committed
243
    mail.parts.first.body.encoded
244
  end
245

246
  # Returns the lft value for a new root issue
247
  def new_issue_lft
248
    1
249
  end
250
end
251

252
module Redmine
jplang's avatar
jplang committed
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
  class MockFile
    attr_reader :size, :original_filename, :content_type
  
    def initialize(options={})
      @size = options[:size] || 32
      @original_filename = options[:original_filename] || options[:filename]
      @content_type = options[:content_type]
      @content = options[:content] || 'x'*size
    end
  
    def read(*args)
      if @eof
        false
      else
        @eof = true
        @content
      end
    end
  end

273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
  class RoutingTest < ActionDispatch::IntegrationTest
    def should_route(arg)
      arg = arg.dup
      request = arg.keys.detect {|key| key.is_a?(String)}
      raise ArgumentError unless request
      options = arg.slice!(request)

      raise ArgumentError unless request =~ /\A(GET|POST|PUT|PATCH|DELETE)\s+(.+)\z/
      method, path = $1.downcase.to_sym, $2

      raise ArgumentError unless arg.values.first =~ /\A(.+)#(.+)\z/
      controller, action = $1, $2

      assert_routing(
        {:method => method, :path => path},
        options.merge(:controller => controller, :action => action)
      )
    end
  end

jplang's avatar
jplang committed
293
  class HelperTest < ActionView::TestCase
294
295
    include Redmine::I18n

296
297
298
    def setup
      super
      User.current = nil
299
      ::I18n.locale = 'en'
300
    end
jplang's avatar
jplang committed
301
302
  end

303
  class ControllerTest < ActionController::TestCase
304
305
306
307
308
309
    # Returns the issues that are displayed in the list in the same order
    def issues_in_list
      ids = css_select('tr.issue td.id').map(&:text).map(&:to_i)
      Issue.where(:id => ids).sort_by {|issue| ids.index(issue.id)}
    end
  
310
    # Return the columns that are displayed in the issue list
311
    def columns_in_issues_list
312
      css_select('table.issues thead th:not(.checkbox)').map(&:text).select(&:present?)
313
314
    end
  
315
316
317
318
    # Return the columns that are displayed in the list
    def columns_in_list
      css_select('table.list thead th:not(.checkbox)').map(&:text).select(&:present?)
    end
319
320
321
322
323
324

    # Returns the values that are displayed in tds with the given css class
    def columns_values_in_list(css_class)
      css_select("table.list tbody td.#{css_class}").map(&:text)
    end

325
326
327
328
329
330
331
332
333
334
335
    # Verifies that the query filters match the expected filters
    def assert_query_filters(expected_filters)
      response.body =~ /initFilters\(\);\s*((addFilter\(.+\);\s*)*)/
      filter_init = $1.to_s
  
      expected_filters.each do |field, operator, values|
        s = "addFilter(#{field.to_json}, #{operator.to_json}, #{Array(values).to_json});"
        assert_include s, filter_init
      end
      assert_equal expected_filters.size, filter_init.scan("addFilter").size, "filters counts don't match"
    end
jplang's avatar
jplang committed
336
337
338
339
340
341
342
343
344

    # Saves the generated PDF in tmp/test/pdf
    def save_pdf
      assert_equal 'application/pdf', response.content_type
      filename = "#{self.class.name.underscore}__#{method_name}.pdf"
      File.open(File.join($redmine_tmp_pdf_directory, filename), "wb") do |f|
        f.write response.body
      end
    end
jplang's avatar
jplang committed
345
  end
346

jplang's avatar
jplang committed
347
348
349
350
351
352
353
354
  class RepositoryControllerTest < ControllerTest
    def setup
      super
      # We need to explicitly set Accept header to html otherwise
      # requests that ends with a known format like:
      # GET /projects/foo/repository/entry/image.png would be
      # treated as image/png requests, resulting in a 406 error.
      request.env["HTTP_ACCEPT"] = "text/html"
355
    end
356
357
  end

358
359
360
361
  class IntegrationTest < ActionDispatch::IntegrationTest
    def log_user(login, password)
      User.anonymous
      get "/login"
362
      assert_nil session[:user_id]
363
      assert_response :success
364

jplang's avatar
jplang committed
365
366
367
368
      post "/login", :params => {
          :username => login,
          :password => password
        }
369
370
371
372
373
374
375
376
      assert_equal login, User.find(session[:user_id]).login
    end

    def credentials(user, password=nil)
      {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
    end
  end

377
  module ApiTest
378
379
    API_FORMATS = %w(json xml).freeze

380
    # Base class for API tests
381
    class Base < Redmine::IntegrationTest
382
383
384
385
386
387
388
      def setup
        Setting.rest_api_enabled = '1'
      end

      def teardown
        Setting.rest_api_enabled = '0'
      end
389
390
391
392
393
394
395
396
397
398
399
400
401
402

      # Uploads content using the XML API and returns the attachment token
      def xml_upload(content, credentials)
        upload('xml', content, credentials)
      end

      # Uploads content using the JSON API and returns the attachment token
      def json_upload(content, credentials)
        upload('json', content, credentials)
      end

      def upload(format, content, credentials)
        set_tmp_attachments_directory
        assert_difference 'Attachment.count' do
jplang's avatar
jplang committed
403
404
405
          post "/uploads.#{format}",
            :params => content,
            :headers => {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials)
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
          assert_response :created
        end
        data = response_data
        assert_kind_of Hash, data['upload']
        token = data['upload']['token']
        assert_not_nil token
        token
      end

      # Parses the response body based on its content type
      def response_data
        unless response.content_type.to_s =~ /^application\/(.+)/
          raise "Unexpected response type: #{response.content_type}"
        end
        format = $1
        case format
        when 'xml'
          Hash.from_xml(response.body)
        when 'json'
          ActiveSupport::JSON.decode(response.body)
        else
          raise "Unknown response format: #{format}"
        end
      end
430
    end
431
432
433
434
435
436
437
438
439
440
441
442
443
444

    class Routing < Redmine::RoutingTest
      def should_route(arg)
        arg = arg.dup
        request = arg.keys.detect {|key| key.is_a?(String)}
        raise ArgumentError unless request
        options = arg.slice!(request)
  
        API_FORMATS.each do |format|
          format_request = request.sub /$/, ".#{format}"
          super options.merge(format_request => arg[request], :format => format)
        end
      end
    end
445
  end
446
end