From 092af284766fb1e7d3a8621e9a19cb88d627c888 Mon Sep 17 00:00:00 2001 From: guange <8863824@gmail.com> Date: Mon, 11 May 2015 20:57:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E5=9C=A8=E7=81=AB=E7=8B=90=E4=B8=8B=E7=B2=98=E8=B4=B4=E4=B8=8D?= =?UTF-8?q?=E6=88=90=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/application_helper.rb | 3 +- app/models/token.rb | 2 +- app/views/layouts/_base_feedback.html.erb | 2 + .../lib/rails_kindeditor/helper.rb | 6 +- .../assets/kindeditor/plugins/paste/paste.js | 342 +++++++ public/javascripts/application.js | 2 +- public/javascripts/avatars.js | 9 +- test/integration/account_test.rb | 2 +- test/test_helper.rb | 967 +++++++++--------- 9 files changed, 840 insertions(+), 495 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 405dc72ed..9c261df85 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1770,8 +1770,7 @@ module ApplicationHelper def get_memo @new_memo = Memo.new - #@new_memo.subject = "有什么想说的,尽管来咆哮吧~~" - @public_forum = Forum.find(1) + @public_forum = Forum.find(1) rescue ActiveRecord::RecordNotFound end #获取用户未过期的课程 diff --git a/app/models/token.rb b/app/models/token.rb index f33db62b3..e0accb59c 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -33,7 +33,7 @@ class Token < ActiveRecord::Base unless token token = Token.create(:user => user, :action => 'autologin') else - token..update_attribute(:created_on, Time.now) + token.update_attribute(:created_on, Time.now) end token end diff --git a/app/views/layouts/_base_feedback.html.erb b/app/views/layouts/_base_feedback.html.erb index 98a7bb160..9530108f9 100644 --- a/app/views/layouts/_base_feedback.html.erb +++ b/app/views/layouts/_base_feedback.html.erb @@ -168,6 +168,7 @@ function cookieget(n)
<%= l(:label_feedback) %>
+ <% if @public_forum %> <% get_memo %> <%= form_for(@new_memo, :url => create_feedback_forum_path(@public_forum)) do |f| %> <%= f.text_area :subject,:id=>"subject", :class => "opnionText", :placeholder => l(:label_feedback_tips) %> @@ -176,6 +177,7 @@ function cookieget(n) <%= l(:label_submit)%> <% end %> + <% end %>
<%= l(:label_technical_support) %>白   羽 diff --git a/lib/rails_kindeditor/lib/rails_kindeditor/helper.rb b/lib/rails_kindeditor/lib/rails_kindeditor/helper.rb index 56b4c7adf..f5c85677f 100644 --- a/lib/rails_kindeditor/lib/rails_kindeditor/helper.rb +++ b/lib/rails_kindeditor/lib/rails_kindeditor/helper.rb @@ -54,9 +54,9 @@ module RailsKindeditor if(old_onload_#{random_name}) old_onload_#{random_name}(); }" else - "KindEditor.ready(function(K){ - #{editor_id}K.create('##{dom_id}', #{get_options(options).to_json}).loadPlugin('paste'); - });" + "$(function(){KindEditor.ready(function(K){ + #{editor_id}K.create('##{dom_id}', #{get_options(options).to_json}).loadPlugin('paste'); + });});" end end diff --git a/public/assets/kindeditor/plugins/paste/paste.js b/public/assets/kindeditor/plugins/paste/paste.js index 0f63ce18c..f2d080dcf 100644 --- a/public/assets/kindeditor/plugins/paste/paste.js +++ b/public/assets/kindeditor/plugins/paste/paste.js @@ -1,7 +1,348 @@ +// Generated by CoffeeScript 1.9.0 + +/* +paste.js is an interface to read data ( text / image ) from clipboard in different browsers. It also contains several hacks. +https://github.com/layerssss/paste.js + */ + +(function() { + var $, Paste, createHiddenEditable, dataURLtoBlob; + + $ = window.jQuery; + + $.paste = function(pasteContainer) { + var pm; + if (typeof console !== "undefined" && console !== null) { + console.log("DEPRECATED: This method is deprecated. Please use $.fn.pastableNonInputable() instead."); + } + pm = Paste.mountNonInputable(pasteContainer); + return pm._container; + }; + + $.fn.pastableNonInputable = function() { + var el, _i, _len; + for (_i = 0, _len = this.length; _i < _len; _i++) { + el = this[_i]; + Paste.mountNonInputable(el); + } + return this; + }; + + $.fn.pastableTextarea = function() { + var el, _i, _len; + for (_i = 0, _len = this.length; _i < _len; _i++) { + el = this[_i]; + Paste.mountTextarea(el); + } + return this; + }; + + $.fn.pastableContenteditable = function() { + var el, _i, _len; + for (_i = 0, _len = this.length; _i < _len; _i++) { + el = this[_i]; + Paste.mountContenteditable(el); + } + return this; + }; + + dataURLtoBlob = function(dataURL, sliceSize) { + var b64Data, byteArray, byteArrays, byteCharacters, byteNumbers, contentType, i, m, offset, slice, _ref; + if (sliceSize == null) { + sliceSize = 512; + } + if (!(m = dataURL.match(/^data\:([^\;]+)\;base64\,(.+)$/))) { + return null; + } + _ref = m, m = _ref[0], contentType = _ref[1], b64Data = _ref[2]; + byteCharacters = atob(b64Data); + byteArrays = []; + offset = 0; + while (offset < byteCharacters.length) { + slice = byteCharacters.slice(offset, offset + sliceSize); + byteNumbers = new Array(slice.length); + i = 0; + while (i < slice.length) { + byteNumbers[i] = slice.charCodeAt(i); + i++; + } + byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + offset += sliceSize; + } + return new Blob(byteArrays, { + type: contentType + }); + }; + + createHiddenEditable = function() { + return $(document.createElement('div')).attr('contenteditable', true).css({ + width: 1, + height: 1, + position: 'fixed', + left: -100, + overflow: 'hidden' + }); + }; + + Paste = (function() { + Paste.prototype._target = null; + + Paste.prototype._container = null; + + Paste.mountNonInputable = function(nonInputable) { + var paste; + paste = new Paste(createHiddenEditable().appendTo(nonInputable), nonInputable); + $(nonInputable).on('click', (function(_this) { + return function() { + return paste._container.focus(); + }; + })(this)); + paste._container.on('focus', (function(_this) { + return function() { + return $(nonInputable).addClass('pastable-focus'); + }; + })(this)); + return paste._container.on('blur', (function(_this) { + return function() { + return $(nonInputable).removeClass('pastable-focus'); + }; + })(this)); + }; + + Paste.mountTextarea = function(textarea) { + var ctlDown, paste; + if (-1 !== navigator.userAgent.toLowerCase().indexOf('chrome')) { + return this.mountContenteditable(textarea); + } + paste = new Paste(createHiddenEditable().insertBefore(textarea), textarea); + ctlDown = false; + $(textarea).on('keyup', function(ev) { + var _ref; + if ((_ref = ev.keyCode) === 17 || _ref === 224) { + return ctlDown = false; + } + }); + $(textarea).on('keydown', function(ev) { + var _ref; + if ((_ref = ev.keyCode) === 17 || _ref === 224) { + ctlDown = true; + } + if (ctlDown && ev.keyCode === 86) { + return paste._container.focus(); + } + }); + $(paste._target).on('pasteImage', (function(_this) { + return function() { + return $(textarea).focus(); + }; + })(this)); + $(paste._target).on('pasteText', (function(_this) { + return function() { + return $(textarea).focus(); + }; + })(this)); + $(textarea).on('focus', (function(_this) { + return function() { + return $(textarea).addClass('pastable-focus'); + }; + })(this)); + return $(textarea).on('blur', (function(_this) { + return function() { + return $(textarea).removeClass('pastable-focus'); + }; + })(this)); + }; + + Paste.mountContenteditable = function(contenteditable) { + var paste; + paste = new Paste(contenteditable, contenteditable); + $(contenteditable).on('focus', (function(_this) { + return function() { + return $(contenteditable).addClass('pastable-focus'); + }; + })(this)); + return $(contenteditable).on('blur', (function(_this) { + return function() { + return $(contenteditable).removeClass('pastable-focus'); + }; + })(this)); + }; + + function Paste(_at__container, _at__target) { + this._container = _at__container; + this._target = _at__target; + this._container = $(this._container); + this._target = $(this._target).addClass('pastable'); + this._container.on('paste', (function(_this) { + return function(ev) { + var clipboardData, file, item, reader, text, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3, _results; + if (((_ref = ev.originalEvent) != null ? _ref.clipboardData : void 0) != null) { + clipboardData = ev.originalEvent.clipboardData; + if (clipboardData.items) { + _ref1 = clipboardData.items; + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + item = _ref1[_i]; + if (item.type.match(/^image\//)) { + reader = new FileReader(); + reader.onload = function(event) { + return _this._handleImage(event.target.result); + }; + reader.readAsDataURL(item.getAsFile()); + } + if (item.type === 'text/plain') { + item.getAsString(function(string) { + return _this._target.trigger('pasteText', { + text: string + }); + }); + } + } + } else { + if (-1 !== Array.prototype.indexOf.call(clipboardData.types, 'text/plain')) { + text = clipboardData.getData('Text'); + _this._target.trigger('pasteText', { + text: text + }); + } + _this._checkImagesInContainer(function(src) { + return _this._handleImage(src); + }); + } + } + if (clipboardData = window.clipboardData) { + if ((_ref2 = (text = clipboardData.getData('Text'))) != null ? _ref2.length : void 0) { + return _this._target.trigger('pasteText', { + text: text + }); + } else { + _ref3 = clipboardData.files; + _results = []; + for (_j = 0, _len1 = _ref3.length; _j < _len1; _j++) { + file = _ref3[_j]; + _this._handleImage(URL.createObjectURL(file)); + _results.push(_this._checkImagesInContainer(function() {})); + } + return _results; + } + } + }; + })(this)); + } + + Paste.prototype._handleImage = function(src) { + var loader; + loader = new Image(); + loader.onload = (function(_this) { + return function() { + var blob, canvas, ctx, dataURL; + canvas = document.createElement('canvas'); + canvas.width = loader.width; + canvas.height = loader.height; + ctx = canvas.getContext('2d'); + ctx.drawImage(loader, 0, 0, canvas.width, canvas.height); + dataURL = null; + try { + dataURL = canvas.toDataURL('image/png'); + blob = dataURLtoBlob(dataURL); + } catch (_error) {} + if (dataURL) { + return _this._target.trigger('pasteImage', { + blob: blob, + dataURL: dataURL, + width: loader.width, + height: loader.height + }); + } + }; + })(this); + return loader.src = src; + }; + + Paste.prototype._checkImagesInContainer = function(cb) { + var img, timespan, _i, _len, _ref; + timespan = Math.floor(1000 * Math.random()); + _ref = this._container.find('img'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + img = _ref[_i]; + img["_paste_marked_" + timespan] = true; + } + return setTimeout((function(_this) { + return function() { + var _j, _len1, _ref1, _results; + _ref1 = _this._container.find('img'); + _results = []; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + img = _ref1[_j]; + if (!img["_paste_marked_" + timespan]) { + cb(img.src); + } + _results.push($(img).remove()); + } + return _results; + }; + })(this), 1); + }; + + return Paste; + + })(); + +}).call(this); + KindEditor.plugin('paste', function(K) { var editor = this, name = 'paste'; var contentWindow = document.getElementsByTagName('iframe')[0].contentWindow; + var nodeBody = contentWindow.document.getElementsByTagName('body')[0]; + console.log(nodeBody); + $(nodeBody).pastableContenteditable(); + + dataURItoBlob = function(dataURI) { + // convert base64/URLEncoded data component to raw binary data held in a string + var byteString; + if (dataURI.split(',')[0].indexOf('base64') >= 0) + byteString = atob(dataURI.split(',')[1]); + else + byteString = unescape(dataURI.split(',')[1]); + + // separate out the mime component + var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; + + // write the bytes of the string to a typed array + var ia = new Uint8Array(byteString.length); + for (var i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + + return new Blob([ia], {type:mimeString}); + }; + + $(nodeBody).on('pasteImage', function(ev, data) { + console.log('pasteImage'); + console.log("dataURL: " + data.dataURL); + console.log("width: " + data.width); + console.log("height: " + data.height); + console.log(data.blob); + var blob = dataURItoBlob(data.dataURL); + if (data.blob !== null) { + var data = new FormData(); + data.append("imgFile",blob, "imageFilename.png"); + console.log(data); + $.ajax({ + url: '/kindeditor/upload?dir=image', + contentType: false, + type: 'POST', + data: data, + processData: false, + success: function(data) { + editor.exec('insertimage', JSON.parse(data).url); + } + }); + } + + }); + return; contentWindow.document.getElementsByTagName('body')[0].onpaste = function(event) { // use event.originalEvent.clipboard for newer chrome versions var items = (event.clipboardData || event.originalEvent.clipboardData).items; @@ -20,6 +361,7 @@ KindEditor.plugin('paste', function(K) { console.log(event.target.result); // data url! var data = new FormData(); data.append("imgFile", blob, "imageFilename.png"); + console.log(blob); $.ajax({ url: '/kindeditor/upload?dir=image', contentType: false, diff --git a/public/javascripts/application.js b/public/javascripts/application.js index b2bd70d8d..828be910d 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -578,7 +578,7 @@ function setupHeartBeat(){ function setupAjaxIndicator() { $('#ajax-indicator').bind('ajaxSend', function(event, xhr, settings) { - if(settings && settings.url && settings.url.endsWith('account/heartbeat')){ + if(settings && settings.url && settings.url.match(/account\/heartbeat$/)){ return; } if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') { diff --git a/public/javascripts/avatars.js b/public/javascripts/avatars.js index a8f10edf0..571ea4982 100644 --- a/public/javascripts/avatars.js +++ b/public/javascripts/avatars.js @@ -52,13 +52,14 @@ function ajaxUpload(file, fileSpan, inputEl) { fileSpan.removeClass('ajax-loading'); var form = fileSpan.parents('form'); - if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) { - $('input:submit', form).removeAttr('disabled'); - } - form.dequeue('upload'); + // if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) { + // $('input:submit', form).removeAttr('disabled'); + // } + // form.dequeue('upload'); }); } + var progressSpan = $('#upload_progressbar'); progressSpan.progressbar(); fileSpan.addClass('ajax-waiting'); diff --git a/test/integration/account_test.rb b/test/integration/account_test.rb index 7562bcf7c..624c91f13 100644 --- a/test/integration/account_test.rb +++ b/test/integration/account_test.rb @@ -29,7 +29,7 @@ class AccountTest < ActionController::IntegrationTest # Replace this with your real tests. def test_login get "my/page" - assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage" + assert_redirected_to "/login" log_user('jsmith', 'jsmith') get "my/account" diff --git a/test/test_helper.rb b/test/test_helper.rb index 9b1a4f158..b7d786704 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,22 +1,22 @@ require 'rubygems' -require 'spork' +# require 'spork' #uncomment the following line to use spork with the debugger #require 'spork/ext/ruby-debug' -Spork.prefork do - # Loading more in this block will cause your tests to run faster. However, - # if you change any configuration or code from libraries loaded here, you'll - # need to restart spork for it take effect. - ENV["RAILS_ENV"] = "test" - require File.expand_path('../../config/environment', __FILE__) - require 'rails/test_help' - -end - -Spork.each_run do - # This code will be run each time you run your specs. - -end +# Spork.prefork do +# # Loading more in this block will cause your tests to run faster. However, +# # if you change any configuration or code from libraries loaded here, you'll +# # need to restart spork for it take effect. +# ENV["RAILS_ENV"] = "test" +# require File.expand_path('../../config/environment', __FILE__) +# require 'rails/test_help' +# +# end +# +# Spork.each_run do +# # This code will be run each time you run your specs. +# +# end # --- Instructions --- # Sort the contents of this file into a Spork.prefork and a Spork.each_run @@ -50,471 +50,472 @@ end -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# 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. -# -# 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. -# -# 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. - -#require 'shoulda' -ENV["RAILS_ENV"] = "test" -require File.expand_path(File.dirname(__FILE__) + "/../config/environment") -require 'rails/test_help' -require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s - -require File.expand_path(File.dirname(__FILE__) + '/object_helpers') -include ObjectHelpers - -class ActiveSupport::TestCase - include ActionDispatch::TestProcess - - self.use_transactional_fixtures = true - self.use_instantiated_fixtures = false - - def log_user(login, password) - User.anonymous - get "/login" - assert_equal nil, session[:user_id] - assert_response :success - assert_template "account/login" - post "/login", :username => login, :password => password - assert_equal login, User.find(session[:user_id]).login - end - - def uploaded_test_file(name, mime) - fixture_file_upload("files/#{name}", mime, true) - end - - def credentials(user, password=nil) - {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)} - end - - # Mock out a file - def self.mock_file - file = 'a_file.png' - file.stubs(:size).returns(32) - file.stubs(:original_filename).returns('a_file.png') - file.stubs(:content_type).returns('image/png') - file.stubs(:read).returns(false) - file - end - - def mock_file - self.class.mock_file - end - - def mock_file_with_options(options={}) - file = '' - file.stubs(:size).returns(32) - original_filename = options[:original_filename] || nil - file.stubs(:original_filename).returns(original_filename) - content_type = options[:content_type] || nil - file.stubs(:content_type).returns(content_type) - file.stubs(:read).returns(false) - file - end - - # Use a temporary directory for attachment related tests - def set_tmp_attachments_directory - Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test") - unless File.directory?("#{Rails.root}/tmp/test/attachments") - Dir.mkdir "#{Rails.root}/tmp/test/attachments" - end - Attachment.storage_path = "#{Rails.root}/tmp/test/attachments" - end - - def set_fixtures_attachments_directory - Attachment.storage_path = "#{Rails.root}/test/fixtures/files" - end - - def with_settings(options, &block) - 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 - options.each {|k, v| Setting[k] = v} - yield - ensure - saved_settings.each {|k, v| Setting[k] = v} if saved_settings - end - - # 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 - - def change_user_password(login, new_password) - user = User.first(:conditions => {:login => login}) - user.password, user.password_confirmation = new_password, new_password - user.save! - end - - def self.ldap_configured? - @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389) - return @test_ldap.bind - rescue Exception => e - # LDAP is not listening - return nil - end - - def self.convert_installed? - Redmine::Thumbnail.convert_available? - end - - # Returns the path to the test +vendor+ repository - def self.repository_path(vendor) - Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s - end - - # Returns the url of the subversion test repository - def self.subversion_repository_url - path = repository_path('subversion') - path = '/' + path unless path.starts_with?('/') - "file://#{path}" - end - - # Returns true if the +vendor+ test repository is configured - def self.repository_configured?(vendor) - File.directory?(repository_path(vendor)) - end - - def repository_path_hash(arr) - hs = {} - hs[:path] = arr.join("/") - hs[:param] = arr.join("/") - hs - end - - 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 - - def assert_error_tag(options={}) - assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options)) - end - - def assert_include(expected, s, message=nil) - assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"") - end - - def assert_not_include(expected, s) - assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\"" - end - - def assert_select_in(text, *args, &block) - d = HTML::Document.new(CGI::unescapeHTML(String.new(text))).root - assert_select(d, *args, &block) - end - - def assert_mail_body_match(expected, mail) - if expected.is_a?(String) - assert_include expected, mail_body(mail) - else - assert_match expected, mail_body(mail) - end - end - - def assert_mail_body_no_match(expected, mail) - if expected.is_a?(String) - assert_not_include expected, mail_body(mail) - else - assert_no_match expected, mail_body(mail) - end - end - - def mail_body(mail) - mail.parts.first.body.encoded - end -end - -module Redmine - module ApiTest - # Base class for API tests - class Base < ActionDispatch::IntegrationTest - # Test that a request allows the three types of API authentication - # - # * HTTP Basic with username and password - # * HTTP Basic with an api key for the username - # * Key based with the key=X parameter - # - # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) - # @param [String] url the request url - # @param [optional, Hash] parameters additional request parameters - # @param [optional, Hash] options additional options - # @option options [Symbol] :success_code Successful response code (:success) - # @option options [Symbol] :failure_code Failure response code (:unauthorized) - def self.should_allow_api_authentication(http_method, url, parameters={}, options={}) - should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options) - should_allow_http_basic_auth_with_key(http_method, url, parameters, options) - should_allow_key_based_auth(http_method, url, parameters, options) - end - - # Test that a request allows the username and password for HTTP BASIC - # - # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) - # @param [String] url the request url - # @param [optional, Hash] parameters additional request parameters - # @param [optional, Hash] options additional options - # @option options [Symbol] :success_code Successful response code (:success) - # @option options [Symbol] :failure_code Failure response code (:unauthorized) - def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={}) - success_code = options[:success_code] || :success - failure_code = options[:failure_code] || :unauthorized - - context "should allow http basic auth using a username and password for #{http_method} #{url}" do - context "with a valid HTTP authentication" do - setup do - @user = User.generate! do |user| - user.admin = true - user.password = 'my_password' - end - send(http_method, url, parameters, credentials(@user.login, 'my_password')) - end - - should_respond_with success_code - should_respond_with_content_type_based_on_url(url) - should "login as the user" do - assert_equal @user, User.current - end - end - - context "with an invalid HTTP authentication" do - setup do - @user = User.generate! - send(http_method, url, parameters, credentials(@user.login, 'wrong_password')) - end - - should_respond_with failure_code - should_respond_with_content_type_based_on_url(url) - should "not login as the user" do - assert_equal User.anonymous, User.current - end - end - - context "without credentials" do - setup do - send(http_method, url, parameters) - end - - should_respond_with failure_code - should_respond_with_content_type_based_on_url(url) - should "include_www_authenticate_header" do - assert @controller.response.headers.has_key?('WWW-Authenticate') - end - end - end - end - - # Test that a request allows the API key with HTTP BASIC - # - # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) - # @param [String] url the request url - # @param [optional, Hash] parameters additional request parameters - # @param [optional, Hash] options additional options - # @option options [Symbol] :success_code Successful response code (:success) - # @option options [Symbol] :failure_code Failure response code (:unauthorized) - def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={}) - success_code = options[:success_code] || :success - failure_code = options[:failure_code] || :unauthorized - - context "should allow http basic auth with a key for #{http_method} #{url}" do - context "with a valid HTTP authentication using the API token" do - setup do - @user = User.generate! do |user| - user.admin = true - end - @token = Token.create!(:user => @user, :action => 'api') - send(http_method, url, parameters, credentials(@token.value, 'X')) - end - should_respond_with success_code - should_respond_with_content_type_based_on_url(url) - should_be_a_valid_response_string_based_on_url(url) - should "login as the user" do - assert_equal @user, User.current - end - end - - context "with an invalid HTTP authentication" do - setup do - @user = User.generate! - @token = Token.create!(:user => @user, :action => 'feeds') - send(http_method, url, parameters, credentials(@token.value, 'X')) - end - should_respond_with failure_code - should_respond_with_content_type_based_on_url(url) - should "not login as the user" do - assert_equal User.anonymous, User.current - end - end - end - end - - # Test that a request allows full key authentication - # - # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) - # @param [String] url the request url, without the key=ZXY parameter - # @param [optional, Hash] parameters additional request parameters - # @param [optional, Hash] options additional options - # @option options [Symbol] :success_code Successful response code (:success) - # @option options [Symbol] :failure_code Failure response code (:unauthorized) - def self.should_allow_key_based_auth(http_method, url, parameters={}, options={}) - success_code = options[:success_code] || :success - failure_code = options[:failure_code] || :unauthorized - - context "should allow key based auth using key=X for #{http_method} #{url}" do - context "with a valid api token" do - setup do - @user = User.generate! do |user| - user.admin = true - end - @token = Token.create!(:user => @user, :action => 'api') - # Simple url parse to add on ?key= or &key= - request_url = if url.match(/\?/) - url + "&key=#{@token.value}" - else - url + "?key=#{@token.value}" - end - send(http_method, request_url, parameters) - end - should_respond_with success_code - should_respond_with_content_type_based_on_url(url) - should_be_a_valid_response_string_based_on_url(url) - should "login as the user" do - assert_equal @user, User.current - end - end - - context "with an invalid api token" do - setup do - @user = User.generate! do |user| - user.admin = true - end - @token = Token.create!(:user => @user, :action => 'feeds') - # Simple url parse to add on ?key= or &key= - request_url = if url.match(/\?/) - url + "&key=#{@token.value}" - else - url + "?key=#{@token.value}" - end - send(http_method, request_url, parameters) - end - should_respond_with failure_code - should_respond_with_content_type_based_on_url(url) - should "not login as the user" do - assert_equal User.anonymous, User.current - end - end - end - - context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do - setup do - @user = User.generate! do |user| - user.admin = true - end - @token = Token.create!(:user => @user, :action => 'api') - send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s}) - end - should_respond_with success_code - should_respond_with_content_type_based_on_url(url) - should_be_a_valid_response_string_based_on_url(url) - should "login as the user" do - assert_equal @user, User.current - end - end - end - - # Uses should_respond_with_content_type based on what's in the url: - # - # '/project/issues.xml' => should_respond_with_content_type :xml - # '/project/issues.json' => should_respond_with_content_type :json - # - # @param [String] url Request - def self.should_respond_with_content_type_based_on_url(url) - case - when url.match(/xml/i) - should "respond with XML" do - assert_equal 'application/xml', @response.content_type - end - when url.match(/json/i) - should "respond with JSON" do - assert_equal 'application/json', @response.content_type - end - else - raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}" - end - end - - # Uses the url to assert which format the response should be in - # - # '/project/issues.xml' => should_be_a_valid_xml_string - # '/project/issues.json' => should_be_a_valid_json_string - # - # @param [String] url Request - def self.should_be_a_valid_response_string_based_on_url(url) - case - when url.match(/xml/i) - should_be_a_valid_xml_string - when url.match(/json/i) - should_be_a_valid_json_string - else - raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}" - end - end - - # Checks that the response is a valid JSON string - def self.should_be_a_valid_json_string - should "be a valid JSON string (or empty)" do - assert(response.body.blank? || ActiveSupport::JSON.decode(response.body)) - end - end - - # Checks that the response is a valid XML string - def self.should_be_a_valid_xml_string - should "be a valid XML string" do - assert REXML::Document.new(response.body) - end - end - - def self.should_respond_with(status) - should "respond with #{status}" do - assert_response status - end - end - end - end -end - -# URL helpers do not work with config.threadsafe! -# https://github.com/rspec/rspec-rails/issues/476#issuecomment-4705454 -ActionView::TestCase::TestController.instance_eval do - helper Rails.application.routes.url_helpers -end -ActionView::TestCase::TestController.class_eval do - def _routes - Rails.application.routes - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# 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. +# +# 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. +# +# 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. + +#require 'shoulda' +ENV["RAILS_ENV"] = "test" +require File.expand_path(File.dirname(__FILE__) + "/../config/environment") +require 'rails/test_help' +require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s + +require File.expand_path(File.dirname(__FILE__) + '/object_helpers') +include ObjectHelpers + +class ActiveSupport::TestCase + include ActionDispatch::TestProcess + + self.use_transactional_fixtures = true + self.use_instantiated_fixtures = false + + def log_user(login, password) + User.anonymous + get "/login" + assert_equal nil, session[:user_id] + puts response.response_code() + assert_response :success + assert_template "account/login" + post "/login", :username => login, :password => password + assert_equal login, User.find(session[:user_id]).login + end + + def uploaded_test_file(name, mime) + fixture_file_upload("files/#{name}", mime, true) + end + + def credentials(user, password=nil) + {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)} + end + + # Mock out a file + def self.mock_file + file = 'a_file.png' + file.stubs(:size).returns(32) + file.stubs(:original_filename).returns('a_file.png') + file.stubs(:content_type).returns('image/png') + file.stubs(:read).returns(false) + file + end + + def mock_file + self.class.mock_file + end + + def mock_file_with_options(options={}) + file = '' + file.stubs(:size).returns(32) + original_filename = options[:original_filename] || nil + file.stubs(:original_filename).returns(original_filename) + content_type = options[:content_type] || nil + file.stubs(:content_type).returns(content_type) + file.stubs(:read).returns(false) + file + end + + # Use a temporary directory for attachment related tests + def set_tmp_attachments_directory + Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test") + unless File.directory?("#{Rails.root}/tmp/test/attachments") + Dir.mkdir "#{Rails.root}/tmp/test/attachments" + end + Attachment.storage_path = "#{Rails.root}/tmp/test/attachments" + end + + def set_fixtures_attachments_directory + Attachment.storage_path = "#{Rails.root}/test/fixtures/files" + end + + def with_settings(options, &block) + 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 + options.each {|k, v| Setting[k] = v} + yield + ensure + saved_settings.each {|k, v| Setting[k] = v} if saved_settings + end + + # 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 + + def change_user_password(login, new_password) + user = User.first(:conditions => {:login => login}) + user.password, user.password_confirmation = new_password, new_password + user.save! + end + + def self.ldap_configured? + @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389) + return @test_ldap.bind + rescue Exception => e + # LDAP is not listening + return nil + end + + def self.convert_installed? + Redmine::Thumbnail.convert_available? + end + + # Returns the path to the test +vendor+ repository + def self.repository_path(vendor) + Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s + end + + # Returns the url of the subversion test repository + def self.subversion_repository_url + path = repository_path('subversion') + path = '/' + path unless path.starts_with?('/') + "file://#{path}" + end + + # Returns true if the +vendor+ test repository is configured + def self.repository_configured?(vendor) + File.directory?(repository_path(vendor)) + end + + def repository_path_hash(arr) + hs = {} + hs[:path] = arr.join("/") + hs[:param] = arr.join("/") + hs + end + + 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 + + def assert_error_tag(options={}) + assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options)) + end + + def assert_include(expected, s, message=nil) + assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"") + end + + def assert_not_include(expected, s) + assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\"" + end + + def assert_select_in(text, *args, &block) + d = HTML::Document.new(CGI::unescapeHTML(String.new(text))).root + assert_select(d, *args, &block) + end + + def assert_mail_body_match(expected, mail) + if expected.is_a?(String) + assert_include expected, mail_body(mail) + else + assert_match expected, mail_body(mail) + end + end + + def assert_mail_body_no_match(expected, mail) + if expected.is_a?(String) + assert_not_include expected, mail_body(mail) + else + assert_no_match expected, mail_body(mail) + end + end + + def mail_body(mail) + mail.parts.first.body.encoded + end +end + +module Redmine + module ApiTest + # Base class for API tests + class Base < ActionDispatch::IntegrationTest + # Test that a request allows the three types of API authentication + # + # * HTTP Basic with username and password + # * HTTP Basic with an api key for the username + # * Key based with the key=X parameter + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_api_authentication(http_method, url, parameters={}, options={}) + should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options) + should_allow_http_basic_auth_with_key(http_method, url, parameters, options) + should_allow_key_based_auth(http_method, url, parameters, options) + end + + # Test that a request allows the username and password for HTTP BASIC + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={}) + success_code = options[:success_code] || :success + failure_code = options[:failure_code] || :unauthorized + + context "should allow http basic auth using a username and password for #{http_method} #{url}" do + context "with a valid HTTP authentication" do + setup do + @user = User.generate! do |user| + user.admin = true + user.password = 'my_password' + end + send(http_method, url, parameters, credentials(@user.login, 'my_password')) + end + + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + + context "with an invalid HTTP authentication" do + setup do + @user = User.generate! + send(http_method, url, parameters, credentials(@user.login, 'wrong_password')) + end + + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "not login as the user" do + assert_equal User.anonymous, User.current + end + end + + context "without credentials" do + setup do + send(http_method, url, parameters) + end + + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "include_www_authenticate_header" do + assert @controller.response.headers.has_key?('WWW-Authenticate') + end + end + end + end + + # Test that a request allows the API key with HTTP BASIC + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={}) + success_code = options[:success_code] || :success + failure_code = options[:failure_code] || :unauthorized + + context "should allow http basic auth with a key for #{http_method} #{url}" do + context "with a valid HTTP authentication using the API token" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'api') + send(http_method, url, parameters, credentials(@token.value, 'X')) + end + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should_be_a_valid_response_string_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + + context "with an invalid HTTP authentication" do + setup do + @user = User.generate! + @token = Token.create!(:user => @user, :action => 'feeds') + send(http_method, url, parameters, credentials(@token.value, 'X')) + end + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "not login as the user" do + assert_equal User.anonymous, User.current + end + end + end + end + + # Test that a request allows full key authentication + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url, without the key=ZXY parameter + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_key_based_auth(http_method, url, parameters={}, options={}) + success_code = options[:success_code] || :success + failure_code = options[:failure_code] || :unauthorized + + context "should allow key based auth using key=X for #{http_method} #{url}" do + context "with a valid api token" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'api') + # Simple url parse to add on ?key= or &key= + request_url = if url.match(/\?/) + url + "&key=#{@token.value}" + else + url + "?key=#{@token.value}" + end + send(http_method, request_url, parameters) + end + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should_be_a_valid_response_string_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + + context "with an invalid api token" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'feeds') + # Simple url parse to add on ?key= or &key= + request_url = if url.match(/\?/) + url + "&key=#{@token.value}" + else + url + "?key=#{@token.value}" + end + send(http_method, request_url, parameters) + end + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "not login as the user" do + assert_equal User.anonymous, User.current + end + end + end + + context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'api') + send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s}) + end + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should_be_a_valid_response_string_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + end + + # Uses should_respond_with_content_type based on what's in the url: + # + # '/project/issues.xml' => should_respond_with_content_type :xml + # '/project/issues.json' => should_respond_with_content_type :json + # + # @param [String] url Request + def self.should_respond_with_content_type_based_on_url(url) + case + when url.match(/xml/i) + should "respond with XML" do + assert_equal 'application/xml', @response.content_type + end + when url.match(/json/i) + should "respond with JSON" do + assert_equal 'application/json', @response.content_type + end + else + raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}" + end + end + + # Uses the url to assert which format the response should be in + # + # '/project/issues.xml' => should_be_a_valid_xml_string + # '/project/issues.json' => should_be_a_valid_json_string + # + # @param [String] url Request + def self.should_be_a_valid_response_string_based_on_url(url) + case + when url.match(/xml/i) + should_be_a_valid_xml_string + when url.match(/json/i) + should_be_a_valid_json_string + else + raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}" + end + end + + # Checks that the response is a valid JSON string + def self.should_be_a_valid_json_string + should "be a valid JSON string (or empty)" do + assert(response.body.blank? || ActiveSupport::JSON.decode(response.body)) + end + end + + # Checks that the response is a valid XML string + def self.should_be_a_valid_xml_string + should "be a valid XML string" do + assert REXML::Document.new(response.body) + end + end + + def self.should_respond_with(status) + should "respond with #{status}" do + assert_response status + end + end + end + end +end + +# URL helpers do not work with config.threadsafe! +# https://github.com/rspec/rspec-rails/issues/476#issuecomment-4705454 +ActionView::TestCase::TestController.instance_eval do + helper Rails.application.routes.url_helpers +end +ActionView::TestCase::TestController.class_eval do + def _routes + Rails.application.routes + end +end