summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG3
-rw-r--r--app/assets/javascripts/blob/blob.js.coffee73
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee2
-rw-r--r--app/assets/javascripts/line_highlighter.js.coffee148
-rw-r--r--app/controllers/passwords_controller.rb2
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb4
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/user.rb19
-rw-r--r--app/views/admin/users/show.html.haml8
-rw-r--r--app/views/profiles/accounts/show.html.haml2
-rw-r--r--app/views/projects/_issuable_form.html.haml13
-rw-r--r--app/views/shared/_file_highlight.html.haml6
-rw-r--r--doc/raketasks/maintenance.md13
-rw-r--r--doc/ssh/README.md30
-rw-r--r--features/project/source/multiselect_blob.feature85
-rw-r--r--features/snippets/snippets.feature13
-rw-r--r--features/steps/project/source/multiselect_blob.rb58
-rw-r--r--features/steps/shared/authentication.rb4
-rw-r--r--features/steps/snippets/snippets.rb20
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb8
-rw-r--r--spec/factories.rb2
-rw-r--r--spec/features/admin/admin_users_spec.rb28
-rw-r--r--spec/javascripts/fixtures/line_highlighter.html.haml9
-rw-r--r--spec/javascripts/line_highlighter_spec.js.coffee150
-rw-r--r--spec/models/user_spec.rb24
-rw-r--r--spec/support/login_helpers.rb5
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