diff options
27 files changed, 482 insertions, 251 deletions
diff --git a/CHANGELOG b/CHANGELOG index 86de9314d80..d1bf4c1fb0f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.13.0 (unreleased) + - Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu) - Support commenting on diffs in side-by-side mode (Stan Hu) - Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu) - Remove project visibility icons from dashboard projects list @@ -9,6 +10,8 @@ v 7.13.0 (unreleased) - Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8 v 7.12.0 (unreleased) + - Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu) + - Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu) - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) - Update oauth button logos for Twitter and Google to recommended assets - Fix hooks for web based events with external issue references (Daniel Gerhardt) diff --git a/app/assets/javascripts/blob/blob.js.coffee b/app/assets/javascripts/blob/blob.js.coffee deleted file mode 100644 index 37a175fdbc7..00000000000 --- a/app/assets/javascripts/blob/blob.js.coffee +++ /dev/null @@ -1,73 +0,0 @@ -class @BlobView - constructor: -> - # handle multi-line select - handleMultiSelect = (e) -> - [ first_line, last_line ] = parseSelectedLines() - [ line_number ] = parseSelectedLines($(this).attr("id")) - hash = "L#{line_number}" - - if e.shiftKey and not isNaN(first_line) and not isNaN(line_number) - if line_number < first_line - last_line = first_line - first_line = line_number - else - last_line = line_number - - hash = if first_line == last_line then "L#{first_line}" else "L#{first_line}-#{last_line}" - - setHash(hash) - e.preventDefault() - - # See if there are lines selected - # "#L12" and "#L34-56" supported - highlightBlobLines = (e) -> - [ first_line, last_line ] = parseSelectedLines() - - unless isNaN first_line - $("#tree-content-holder .highlight .line").removeClass("hll") - $("#LC#{line}").addClass("hll") for line in [first_line..last_line] - $.scrollTo("#L#{first_line}", offset: -50) unless e? - - # parse selected lines from hash - # always return first and last line (initialized to NaN) - parseSelectedLines = (str) -> - first_line = NaN - last_line = NaN - hash = str || window.location.hash - - if hash isnt "" - matches = hash.match(/\#?L(\d+)(\-(\d+))?/) - first_line = parseInt(matches?[1]) - last_line = parseInt(matches?[3]) - last_line = first_line if isNaN(last_line) - - [ first_line, last_line ] - - setHash = (hash) -> - hash = hash.replace(/^\#/, "") - nodes = $("#" + hash) - # if any nodes are using this id, they must be temporarily changed - # also, add a temporary div at the top of the screen to prevent scrolling - if nodes.length > 0 - scroll_top = $(document).scrollTop() - nodes.attr("id", "") - tmp = $("<div></div>") - .css({ position: "absolute", visibility: "hidden", top: scroll_top + "px" }) - .attr("id", hash) - .appendTo(document.body) - - window.location.hash = hash - - # restore the nodes - if nodes.length > 0 - tmp.remove() - nodes.attr("id", hash) - - # initialize multi-line select - $("#tree-content-holder .line-numbers a[id^=L]").on("click", handleMultiSelect) - - # Highlight the correct lines on load - highlightBlobLines() - - # Highlight the correct lines when the hash part of the URL changes - $(window).on("hashchange", highlightBlobLines) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index b7ebe6a5c89..84873e389ea 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -87,7 +87,7 @@ class Dispatcher new TreeView() shortcut_handler = new ShortcutsNavigation() when 'projects:blob:show' - new BlobView() + new LineHighlighter() shortcut_handler = new ShortcutsNavigation() when 'projects:labels:new', 'projects:labels:edit' new Labels() diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee new file mode 100644 index 00000000000..a8b3c1fa33e --- /dev/null +++ b/app/assets/javascripts/line_highlighter.js.coffee @@ -0,0 +1,148 @@ +# LineHighlighter +# +# Handles single- and multi-line selection and highlight for blob views. +# +#= require jquery.scrollTo +# +# ### Example Markup +# +# <div id="tree-content-holder"> +# <div class="file-content"> +# <div class="line-numbers"> +# <a href="#L1" id="L1" data-line-number="1">1</a> +# <a href="#L2" id="L2" data-line-number="2">2</a> +# <a href="#L3" id="L3" data-line-number="3">3</a> +# <a href="#L4" id="L4" data-line-number="4">4</a> +# <a href="#L5" id="L5" data-line-number="5">5</a> +# </div> +# <pre class="code highlight"> +# <code> +# <span id="LC1" class="line">...</span> +# <span id="LC2" class="line">...</span> +# <span id="LC3" class="line">...</span> +# <span id="LC4" class="line">...</span> +# <span id="LC5" class="line">...</span> +# </code> +# </pre> +# </div> +# </div> +# +class @LineHighlighter + # CSS class applied to highlighted lines + highlightClass: 'hll' + + # Internal copy of location.hash so we're not dependent on `location` in tests + _hash: '' + + # Initialize a LineHighlighter object + # + # hash - String URL hash for dependency injection in tests + constructor: (hash = location.hash) -> + @_hash = hash + + @bindEvents() + + unless hash == '' + range = @hashToRange(hash) + + if range[0] + @highlightRange(range) + + # Scroll to the first highlighted line on initial load + # Offset -50 for the sticky top bar, and another -100 for some context + $.scrollTo("#L#{range[0]}", offset: -150) + + bindEvents: -> + $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler + + # While it may seem odd to bind to the mousedown event and then throw away + # the click event, there is a method to our madness. + # + # If not done this way, the line number anchor will sometimes keep its + # active state even when the event is cancelled, resulting in an ugly border + # around the link and/or a persisted underline text decoration. + + $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) -> + event.preventDefault() + + clickHandler: (event) => + event.preventDefault() + + @clearHighlight() + + lineNumber = $(event.target).data('line-number') + current = @hashToRange(@_hash) + + unless current[0] && event.shiftKey + # If there's no current selection, or there is but Shift wasn't held, + # treat this like a single-line selection. + @setHash(lineNumber) + @highlightLine(lineNumber) + else if event.shiftKey + if lineNumber < current[0] + range = [lineNumber, current[0]] + else + range = [current[0], lineNumber] + + @setHash(range[0], range[1]) + @highlightRange(range) + + # Unhighlight previously highlighted lines + clearHighlight: -> + $(".#{@highlightClass}").removeClass(@highlightClass) + + # Convert a URL hash String into line numbers + # + # hash - Hash String + # + # Examples: + # + # hashToRange('#L5') # => [5, null] + # hashToRange('#L5-15') # => [5, 15] + # hashToRange('#foo') # => [null, null] + # + # Returns an Array + hashToRange: (hash) -> + matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/) + + if matches && matches.length + first = parseInt(matches[1]) + last = if matches[2] then parseInt(matches[2]) else null + + [first, last] + else + [null, null] + + # Highlight a single line + # + # lineNumber - Line number to highlight + highlightLine: (lineNumber) => + $("#LC#{lineNumber}").addClass(@highlightClass) + + # Highlight all lines within a range + # + # range - Array containing the starting and ending line numbers + highlightRange: (range) -> + if range[1] + for lineNumber in [range[0]..range[1]] + @highlightLine(lineNumber) + else + @highlightLine(range[0]) + + # Set the URL hash string + setHash: (firstLineNumber, lastLineNumber) => + if lastLineNumber + hash = "#L#{firstLineNumber}-#{lastLineNumber}" + else + hash = "#L#{firstLineNumber}" + + @_hash = hash + @__setLocationHash__(hash) + + # Make the actual hash change in the browser + # + # This method is stubbed in tests. + __setLocationHash__: (value) -> + # We're using pushState instead of assigning location.hash directly to + # prevent the page from scrolling on the hashchange event + history.pushState({turbolinks: false, url: value}, document.title, value) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 145f27b67dd..8450ba31021 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -24,7 +24,7 @@ class PasswordsController < Devise::PasswordsController super do |resource| # TODO (rspeicher): In Devise master (> 3.4.1), we can set # `Devise.sign_in_after_reset_password = false` and avoid this mess. - if resource.errors.empty? && resource.try(:otp_required_for_login?) + if resource.errors.empty? && resource.try(:two_factor_enabled?) resource.unlock_access! if unlockable?(resource) # Since we are not signing this user in, we use the :updated_not_active diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index e7579c652fb..03845f1e1ec 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def create if current_user.valid_otp?(params[:pin_code]) - current_user.otp_required_for_login = true + current_user.two_factor_enabled = true @codes = current_user.generate_otp_backup_codes! current_user.save! @@ -30,7 +30,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def destroy current_user.update_attributes({ - otp_required_for_login: false, + two_factor_enabled: false, encrypted_otp_secret: nil, encrypted_otp_secret_iv: nil, encrypted_otp_secret_salt: nil, diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 4d976fe6630..7577fc96d6d 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -57,7 +57,7 @@ class SessionsController < Devise::SessionsController def authenticate_with_two_factor user = self.resource = find_user - return unless user && user.otp_required_for_login + return unless user && user.two_factor_enabled? if user_params[:otp_attempt].present? && session[:otp_user_id] if valid_otp_attempt?(user) diff --git a/app/models/ability.rb b/app/models/ability.rb index bcd2adee00b..a5db22040e0 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -263,7 +263,7 @@ class Ability :"modify_#{name}", ] else - if subject.respond_to?(:project) + if subject.respond_to?(:project) && subject.project project_abilities(user, subject.project) else [] diff --git a/app/models/user.rb b/app/models/user.rb index 982c05212ce..a2e2d220b3a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -172,6 +172,9 @@ class User < ActiveRecord::Base after_create :post_create_hook after_destroy :post_destroy_hook + # User's Dashboard preference + # Note: When adding an option, it MUST go on the end of the array. + enum dashboard: [:projects, :stars] alias_attribute :private_token, :authentication_token @@ -297,6 +300,18 @@ class User < ActiveRecord::Base @reset_token end + # Check if the user has enabled Two-factor Authentication + def two_factor_enabled? + otp_required_for_login + end + + # Set whether or not Two-factor Authentication is enabled for the current user + # + # setting - Boolean + def two_factor_enabled=(setting) + self.otp_required_for_login = setting + end + def namespace_uniq namespace_name = self.username existing_namespace = Namespace.by_path(namespace_name) @@ -704,8 +719,4 @@ class User < ActiveRecord::Base def can_be_removed? !solo_owned_groups.present? end - - # User's Dashboard preference - # Note: When adding an option, it MUST go on the end of the array. - enum dashboard: [:projects, :stars] end diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index f7195ac3326..48cd22fc34b 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -50,6 +50,14 @@ = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do %i.fa.fa-times + %li.two-factor-status + %span.light Two-factor Authentication: + %strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'} + - if @user.two_factor_enabled? + Enabled + - else + Disabled + %li %span.light Can create groups: %strong diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index ed009c86568..378dfa2dce0 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -36,7 +36,7 @@ .panel-heading Two-factor Authentication .panel-body - - if current_user.otp_required_for_login + - if current_user.two_factor_enabled? .pull-right = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm', data: { confirm: 'Are you sure?' } diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 4d93c89c93a..496fad34dc2 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -15,10 +15,10 @@ - if issuable.is_a?(MergeRequest) %p.help-block - if issuable.work_in_progress? - Remove the <code>WIP</code> prefix from the title to allow this + Remove the <code>WIP</code> prefix from the title to allow this <strong>Work In Progress</strong> merge request to be accepted when it's ready. - else - Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a + Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a <strong>Work In Progress</strong> merge request from being accepted before it's ready. .form-group.issuable-description = f.label :description, 'Description', class: 'control-label' @@ -81,21 +81,22 @@ - if issuable.is_a?(MergeRequest) %hr - - unless @merge_request.persisted? + - if @merge_request.new_record? .form-group = f.label :source_branch, class: 'control-label' do %i.fa.fa-code-fork Source Branch .col-sm-10 = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) - %p.help-block - = link_to 'Change source branch', mr_change_branches_path(@merge_request) .form-group = f.label :target_branch, class: 'control-label' do %i.fa.fa-code-fork Target Branch .col-sm-10 - = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2' }) + = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? }) + - if @merge_request.new_record? + %p.help-block + = link_to 'Change branches', mr_change_branches_path(@merge_request) .form-actions - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted? diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 86921f0a777..d6a2e177da1 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,11 +1,11 @@ .file-content.code{class: user_color_scheme_class} .line-numbers - if blob.data.present? - - blob.data.lines.to_a.size.times do |index| + - blob.data.lines.each_index do |index| - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset - / We're not using `link_to` because it is too slow once we get to thousands of lines. - %a{href: "#L#{i}", id: "L#{i}", rel: "#L#{i}"} + -# We're not using `link_to` because it is too slow once we get to thousands of lines. + %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} %i.fa.fa-link = i :preserve diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 2aca91d5371..69171cd1765 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -165,13 +165,18 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production Sometimes during version upgrades you might end up with some wrong CSS or missing some icons. In that case, try to precompile the assets again. -For Omnibus-packages: -``` -sudo gitlab-rake assets:precompile -``` +Note that this only applies to source installations and does NOT apply to +omnibus packages. For installations from source: ``` cd /home/git/gitlab sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ``` + +For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at +the release of upstream GitLab. The omnibus version includes optimized versions +of those assets. Unless you are modifying the JavaScript / CSS code on your +production machine after installing the package, there should be no reason to redo +rake assets:precompile on the production machine. If you suspect that assets +have been corrupted, you should reinstall the omnibus package. diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 0acf92fbf54..5f44f9351dd 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -30,7 +30,7 @@ cat ~/.ssh/id_rsa.pub Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your user profile. Please copy the complete key starting with `ssh-` and ending -with your username and host. +with your username and host. Use code below to copy your public key to the clipboard. Depending on your OS you'll need to use a different command: @@ -77,3 +77,31 @@ information. ### Eclipse How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration + +## Tip: Non-default OpenSSH key file names or locations + +If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: + +``` +# +# Main gitlab.com server +# +Host gitlab.com +RSAAuthentication yes +IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename +User mygitlabusername +``` + +Another example +``` +# +# Our company's internal Gitlab server +# +Host my-gitlab.company.com +RSAAuthentication yes +IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename +``` + +Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ. + +Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document. diff --git a/features/project/source/multiselect_blob.feature b/features/project/source/multiselect_blob.feature deleted file mode 100644 index 63b7cb77a93..00000000000 --- a/features/project/source/multiselect_blob.feature +++ /dev/null @@ -1,85 +0,0 @@ -Feature: Project Source Multiselect Blob - Background: - Given I sign in as a user - And I own project "Shop" - And I visit ".gitignore" file in repo - - @javascript - Scenario: I click line 1 in file - When I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I shift-click line 1 in file - When I shift-click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I click line 1 then click line 2 in file - When I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I click line 2 in file - Then I should see "L2" as URI fragment - And I should see line 2 highlighted - - @javascript - Scenario: I click various line numbers to test multiselect - Then I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I shift-click line 2 in file - Then I should see "L1-2" as URI fragment - And I should see lines 1-2 highlighted - Then I shift-click line 3 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I click line 3 in file - Then I should see "L3" as URI fragment - And I should see line 3 highlighted - Then I shift-click line 1 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I shift-click line 5 in file - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted - Then I shift-click line 4 in file - Then I should see "L1-4" as URI fragment - And I should see lines 1-4 highlighted - Then I click line 5 in file - Then I should see "L5" as URI fragment - And I should see line 5 highlighted - Then I shift-click line 3 in file - Then I should see "L3-5" as URI fragment - And I should see lines 3-5 highlighted - Then I shift-click line 1 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I shift-click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I multiselect lines 1-5 and then go back and forward in history - When I click line 1 in file - And I shift-click line 3 in file - And I shift-click line 2 in file - And I shift-click line 5 in file - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted - Then I go back in history - Then I should see "L1-2" as URI fragment - And I should see lines 1-2 highlighted - Then I go back in history - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I go back in history - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I go forward in history - And I go forward in history - And I go forward in history - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature index 6e8019c326f..4f617b6bed8 100644 --- a/features/snippets/snippets.feature +++ b/features/snippets/snippets.feature @@ -25,4 +25,15 @@ Feature: Snippets Scenario: I destroy "Personal snippet one" Given I visit snippet page "Personal snippet one" And I click link "Destroy" - Then I should not see "Personal snippet one" in snippets
\ No newline at end of file + Then I should not see "Personal snippet one" in snippets + + Scenario: I create new internal snippet + Given I logout directly + And I sign in as an admin + Then I visit new snippet page + And I submit new internal snippet + Then I visit snippet page "Internal personal snippet one" + And I logout directly + Then I sign in as a user + Given I visit new snippet page + Then I visit snippet page "Internal personal snippet one" diff --git a/features/steps/project/source/multiselect_blob.rb b/features/steps/project/source/multiselect_blob.rb deleted file mode 100644 index 8e14623b892..00000000000 --- a/features/steps/project/source/multiselect_blob.rb +++ /dev/null @@ -1,58 +0,0 @@ -class Spinach::Features::ProjectSourceMultiselectBlob < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - class << self - def click_line_steps(*line_numbers) - line_numbers.each do |line_number| - step "I click line #{line_number} in file" do - find("#L#{line_number}").click - end - - step "I shift-click line #{line_number} in file" do - script = "$('#L#{line_number}').trigger($.Event('click', { shiftKey: true }));" - execute_script(script) - end - end - end - - def check_state_steps(*ranges) - ranges.each do |range| - fragment = range.kind_of?(Array) ? "L#{range.first}-#{range.last}" : "L#{range}" - pluralization = range.kind_of?(Array) ? "s" : "" - - step "I should see \"#{fragment}\" as URI fragment" do - expect(URI.parse(current_url).fragment).to eq fragment - end - - step "I should see line#{pluralization} #{fragment[1..-1]} highlighted" do - ids = Array(range).map { |n| "LC#{n}" } - extra = false - - highlighted = page.all("#tree-content-holder .highlight .line.hll") - highlighted.each do |element| - extra ||= ids.delete(element[:id]).nil? - end - - expect(extra).to be_false and ids.should be_empty - end - end - end - end - - click_line_steps *Array(1..5) - check_state_steps *Array(1..5), Array(1..2), Array(1..3), Array(1..4), Array(1..5), Array(3..5) - - step 'I go back in history' do - go_back - end - - step 'I go forward in history' do - go_forward - end - - step 'I click on ".gitignore" file in repo' do - click_link ".gitignore" - end -end diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index 3c0f2a9406a..735e0ef6108 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -28,6 +28,10 @@ module SharedAuthentication logout end + step "I logout directly" do + logout_direct + end + def current_user @user || User.first end diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index 09fdd1b5a13..426da2918ea 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -31,6 +31,18 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps click_button "Create snippet" end + step 'I submit new internal snippet' do + fill_in "personal_snippet_title", :with => "Internal personal snippet one" + fill_in "personal_snippet_file_name", :with => "my_snippet.rb" + choose 'personal_snippet_visibility_level_10' + + page.within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of internal snippet' + end + + click_button "Create snippet" + end + step 'I should see snippet "Personal snippet three"' do expect(page).to have_content "Personal snippet three" expect(page).to have_content "Content of snippet three" @@ -58,7 +70,15 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps visit snippet_path(snippet) end + step 'I visit snippet page "Internal personal snippet one"' do + visit snippet_path(internal_snippet) + end + def snippet @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one") end + + def internal_snippet + @snippet ||= PersonalSnippet.find_by!(title: "Internal personal snippet one") + end end diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 65415f21e55..aa09f1a758d 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -40,11 +40,11 @@ describe Profiles::TwoFactorAuthsController do expect(user).to receive(:valid_otp?).with(pin).and_return(true) end - it 'sets otp_required_for_login' do + it 'sets two_factor_enabled' do go user.reload - expect(user.otp_required_for_login).to eq true + expect(user).to be_two_factor_enabled end it 'presents plaintext codes for the user to save' do @@ -109,13 +109,13 @@ describe Profiles::TwoFactorAuthsController do let!(:codes) { user.generate_otp_backup_codes! } it 'clears all 2FA-related fields' do - expect(user.otp_required_for_login).to eq true + expect(user).to be_two_factor_enabled expect(user.otp_backup_codes).not_to be_nil expect(user.encrypted_otp_secret).not_to be_nil delete :destroy - expect(user.otp_required_for_login).to eq false + expect(user).not_to be_two_factor_enabled expect(user.otp_backup_codes).to be_nil expect(user.encrypted_otp_secret).to be_nil end diff --git a/spec/factories.rb b/spec/factories.rb index 9373a7af024..578a2e4dc69 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -30,7 +30,7 @@ FactoryGirl.define do trait :two_factor do before(:create) do |user| - user.otp_required_for_login = true + user.two_factor_enabled = true user.otp_secret = User.generate_otp_secret(32) end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index f97b69713ce..7f5cb30cb94 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -63,15 +63,35 @@ describe "Admin::Users", feature: true do end describe "GET /admin/users/:id" do - before do + it "should have user info" do visit admin_users_path - click_link "#{@user.name}" - end + click_link @user.name - it "should have user info" do expect(page).to have_content(@user.email) expect(page).to have_content(@user.name) end + + describe 'Two-factor Authentication status' do + it 'shows when enabled' do + @user.update_attribute(:two_factor_enabled, true) + + visit admin_user_path(@user) + + expect_two_factor_status('Enabled') + end + + it 'shows when disabled' do + visit admin_user_path(@user) + + expect_two_factor_status('Disabled') + end + + def expect_two_factor_status(status) + page.within('.two-factor-status') do + expect(page).to have_content(status) + end + end + end end describe "GET /admin/users/:id/edit" do diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml new file mode 100644 index 00000000000..15ad1d8968f --- /dev/null +++ b/spec/javascripts/fixtures/line_highlighter.html.haml @@ -0,0 +1,9 @@ +#tree-content-holder + .file-content + .line-numbers + - 1.upto(25) do |i| + %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}= i + %pre.code.highlight + %code + - 1.upto(25) do |i| + %span.line{id: "LC#{i}"}= "Line #{i}" diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee new file mode 100644 index 00000000000..14fa487ff7f --- /dev/null +++ b/spec/javascripts/line_highlighter_spec.js.coffee @@ -0,0 +1,150 @@ +#= require line_highlighter + +describe 'LineHighlighter', -> + fixture.preload('line_highlighter.html') + + clickLine = (number, eventData = {}) -> + if $.isEmptyObject(eventData) + $("#L#{number}").mousedown().click() + else + e = $.Event 'mousedown', eventData + $("#L#{number}").trigger(e).click() + + beforeEach -> + fixture.load('line_highlighter.html') + @class = new LineHighlighter() + @css = @class.highlightClass + @spies = { + __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake -> + } + + describe 'behavior', -> + it 'highlights one line given in the URL hash', -> + new LineHighlighter('#L13') + expect($('#LC13')).toHaveClass(@css) + + it 'highlights a range of lines given in the URL hash', -> + new LineHighlighter('#L5-25') + expect($(".#{@css}").length).toBe(21) + expect($("#LC#{line}")).toHaveClass(@css) for line in [5..25] + + it 'scrolls to the first highlighted line on initial load', -> + spy = spyOn($, 'scrollTo') + new LineHighlighter('#L5-25') + expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()) + + it 'discards click events', -> + spy = spyOnEvent('a[data-line-number]', 'click') + clickLine(13) + expect(spy).toHaveBeenPrevented() + + it 'handles garbage input from the hash', -> + func = -> new LineHighlighter('#tree-content-holder') + expect(func).not.toThrow() + + describe '#clickHandler', -> + it 'discards the mousedown event', -> + spy = spyOnEvent('a[data-line-number]', 'mousedown') + clickLine(13) + expect(spy).toHaveBeenPrevented() + + describe 'without shiftKey', -> + it 'highlights one line when clicked', -> + clickLine(13) + expect($('#LC13')).toHaveClass(@css) + + it 'unhighlights previously highlighted lines', -> + clickLine(13) + clickLine(20) + + expect($('#LC13')).not.toHaveClass(@css) + expect($('#LC20')).toHaveClass(@css) + + it 'sets the hash', -> + spy = spyOn(@class, 'setHash').and.callThrough() + clickLine(13) + expect(spy).toHaveBeenCalledWith(13) + + describe 'with shiftKey', -> + it 'sets the hash', -> + spy = spyOn(@class, 'setHash').and.callThrough() + clickLine(13) + clickLine(20, shiftKey: true) + expect(spy).toHaveBeenCalledWith(13) + expect(spy).toHaveBeenCalledWith(13, 20) + + describe 'without existing highlight', -> + it 'highlights the clicked line', -> + clickLine(13, shiftKey: true) + expect($('#LC13')).toHaveClass(@css) + expect($(".#{@css}").length).toBe(1) + + it 'sets the hash', -> + spy = spyOn(@class, 'setHash') + clickLine(13, shiftKey: true) + expect(spy).toHaveBeenCalledWith(13) + + describe 'with existing single-line highlight', -> + it 'uses existing line as last line when target is lesser', -> + clickLine(20) + clickLine(15, shiftKey: true) + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [15..20] + + it 'uses existing line as first line when target is greater', -> + clickLine(5) + clickLine(10, shiftKey: true) + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] + + describe 'with existing multi-line highlight', -> + beforeEach -> + clickLine(10, shiftKey: true) + clickLine(13, shiftKey: true) + + it 'uses target as first line when it is less than existing first line', -> + clickLine(5, shiftKey: true) + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] + + it 'uses target as last line when it is greater than existing first line', -> + clickLine(15, shiftKey: true) + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [10..15] + + describe '#hashToRange', -> + beforeEach -> + @subject = @class.hashToRange + + it 'extracts a single line number from the hash', -> + expect(@subject('#L5')).toEqual([5, null]) + + it 'extracts a range of line numbers from the hash', -> + expect(@subject('#L5-15')).toEqual([5, 15]) + + it 'returns [null, null] when the hash is not a line number', -> + expect(@subject('#foo')).toEqual([null, null]) + + describe '#highlightLine', -> + beforeEach -> + @subject = @class.highlightLine + + it 'highlights the specified line', -> + @subject(13) + expect($('#LC13')).toHaveClass(@css) + + it 'accepts a String-based number', -> + @subject('13') + expect($('#LC13')).toHaveClass(@css) + + describe '#setHash', -> + beforeEach -> + @subject = @class.setHash + + it 'sets the location hash for a single line', -> + @subject(5) + expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5') + + it 'sets the location hash for a range', -> + @subject(5, 15) + expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15') diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f3e278e5c5f..fa7680fbbec 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -210,6 +210,30 @@ describe User do end end + describe '#two_factor_enabled' do + it 'returns two-factor authentication status' do + enabled = build_stubbed(:user, two_factor_enabled: true) + disabled = build_stubbed(:user) + + expect(enabled).to be_two_factor_enabled + expect(disabled).not_to be_two_factor_enabled + end + end + + describe '#two_factor_enabled=' do + it 'enables two-factor authentication' do + user = build_stubbed(:user, two_factor_enabled: false) + expect { user.two_factor_enabled = true }. + to change { user.two_factor_enabled? }.to(true) + end + + it 'disables two-factor authentication' do + user = build_stubbed(:user, two_factor_enabled: true) + expect { user.two_factor_enabled = false }. + to change { user.two_factor_enabled? }.to(false) + end + end + describe 'authentication token' do it "should have authentication token" do user = create(:user) diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 1bd68552012..ffe30a4246c 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -39,4 +39,9 @@ module LoginHelpers def logout find(:css, ".fa.fa-sign-out").click end + + # Logout without JavaScript driver + def logout_direct + page.driver.submit :delete, '/users/sign_out', {} + end end |