From d7ed3b4766871c30f50736c1d9eedc46c4035841 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 1 Jun 2020 18:08:07 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- Gemfile.lock | 2 +- .../boards/components/board_sidebar.js | 2 +- app/channels/application_cable/channel.rb | 11 ++ app/channels/application_cable/connection.rb | 8 + app/channels/application_cable/logging.rb | 17 ++ app/controllers/concerns/known_sign_in.rb | 2 +- app/mailers/emails/profile.rb | 11 +- app/mailers/previews/notify_preview.rb | 2 +- app/models/active_session.rb | 2 +- app/services/issuable/bulk_update_service.rb | 8 +- app/services/notification_service.rb | 4 +- .../notify/unknown_sign_in_email.html.haml | 66 ++++++-- cable/config.ru | 2 + ...ssing-new-sign-in-email-beautification.yml | 5 + config/initializers/lograge.rb | 6 +- doc/administration/logs.md | 23 +++ doc/api/groups.md | 12 ++ doc/ci/variables/README.md | 7 + doc/ci/yaml/README.md | 8 + doc/development/fe_guide/graphql.md | 89 ++++++++++- doc/development/go_guide/dependencies.md | 12 +- doc/development/i18n/proofreader.md | 1 + doc/user/packages/go_proxy/index.md | 148 +++++++++--------- .../img/unknown_sign_in_email_v13_1.png | Bin 0 -> 20230 bytes .../profile/unknown_sign_in_notification.md | 4 +- doc/user/project/releases/index.md | 2 + lib/gitlab/lograge/custom_options.rb | 6 +- locale/gitlab.pot | 15 +- spec/channels/issues_channel_spec.rb | 4 +- spec/features/action_cable_logging_spec.rb | 37 +++++ spec/initializers/lograge_spec.rb | 2 +- .../lib/gitlab/lograge/custom_options_spec.rb | 45 +++--- spec/mailers/emails/profile_spec.rb | 32 ++-- spec/models/active_session_spec.rb | 2 +- spec/services/notification_service_spec.rb | 5 +- spec/spec_helper.rb | 1 + spec/support/action_cable.rb | 7 + .../helpers/stub_action_cable_connection.rb | 7 + 38 files changed, 453 insertions(+), 164 deletions(-) create mode 100644 app/channels/application_cable/logging.rb create mode 100644 changelogs/unreleased/dblessing-new-sign-in-email-beautification.yml create mode 100644 doc/user/profile/img/unknown_sign_in_email_v13_1.png create mode 100644 spec/features/action_cable_logging_spec.rb create mode 100644 spec/support/action_cable.rb create mode 100644 spec/support/helpers/stub_action_cable_connection.rb diff --git a/Gemfile.lock b/Gemfile.lock index 03d492651ed..92bc594a7d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -604,7 +604,7 @@ GEM ruby_dep (~> 1.2) locale (2.1.2) lockbox (0.3.3) - lograge (0.10.0) + lograge (0.11.2) actionpack (>= 4) activesupport (>= 4) railties (>= 4) diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index c8953158811..056a7b48212 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -54,7 +54,7 @@ export default Vue.extend({ return this.issue.milestone ? this.issue.milestone.title : __('No milestone'); }, canRemove() { - return !this.list.preset; + return !this.list?.preset; }, hasLabels() { return this.issue.labels && this.issue.labels.length; diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb index 9aec2305390..0de2b0185b5 100644 --- a/app/channels/application_cable/channel.rb +++ b/app/channels/application_cable/channel.rb @@ -2,5 +2,16 @@ module ApplicationCable class Channel < ActionCable::Channel::Base + include Logging + + private + + def notification_payload(_) + super.merge!(params: params.except(:channel)) + end + + def request + connection.request + end end end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 87c833f3593..1361269f2a2 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -2,8 +2,12 @@ module ApplicationCable class Connection < ActionCable::Connection::Base + include Logging + identified_by :current_user + public :request + def connect self.current_user = find_user_from_session_store end @@ -18,5 +22,9 @@ module ApplicationCable def session_id Rack::Session::SessionId.new(cookies[Gitlab::Application.config.session_options[:key]]) end + + def notification_payload(_) + super.merge!(params: request.params) + end end end diff --git a/app/channels/application_cable/logging.rb b/app/channels/application_cable/logging.rb new file mode 100644 index 00000000000..4152f8c779f --- /dev/null +++ b/app/channels/application_cable/logging.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ApplicationCable + module Logging + private + + def notification_payload(_) + super.merge!( + Labkit::Correlation::CorrelationId::LOG_KEY => request.request_id, + user_id: current_user&.id, + username: current_user&.username, + remote_ip: request.remote_ip, + ua: request.env['HTTP_USER_AGENT'] + ) + end + end +end diff --git a/app/controllers/concerns/known_sign_in.rb b/app/controllers/concerns/known_sign_in.rb index 97883d8d08c..c0b9605de58 100644 --- a/app/controllers/concerns/known_sign_in.rb +++ b/app/controllers/concerns/known_sign_in.rb @@ -26,6 +26,6 @@ module KnownSignIn end def notify_user - current_user.notification_service.unknown_sign_in(current_user, request.remote_ip) + current_user.notification_service.unknown_sign_in(current_user, request.remote_ip, current_user.current_sign_in_at) end end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 4b19149a833..c327a0bab43 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -45,13 +45,20 @@ module Emails end end - def unknown_sign_in_email(user, ip) + def unknown_sign_in_email(user, ip, time) @user = user @ip = ip + @time = time @target_url = edit_profile_password_url Gitlab::I18n.with_locale(@user.preferred_language) do - mail(to: @user.notification_email, subject: subject(_("Unknown sign-in from new location"))) + mail( + to: @user.notification_email, + subject: subject(_("%{host} sign-in from new location") % { host: Gitlab.config.gitlab.host }) + ) do |format| + format.html { render layout: 'mailer' } + format.text { render layout: 'mailer' } + end end end end diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index c931b5a848f..cb7c6a36c27 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -162,7 +162,7 @@ class NotifyPreview < ActionMailer::Preview end def unknown_sign_in_email - Notify.unknown_sign_in_email(user, '127.0.0.1').message + Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message end private diff --git a/app/models/active_session.rb b/app/models/active_session.rb index 065bd5507be..a23190cc8b3 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -36,7 +36,7 @@ class ActiveSession timestamp = Time.current active_user_session = new( - ip_address: request.ip, + ip_address: request.remote_ip, browser: client.name, os: client.os_name, device_name: client.device_name, diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index 2cd0e1e992d..1518b697f86 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -40,9 +40,13 @@ module Issuable private def permitted_attrs(type) - attrs = %i(state_event milestone_id assignee_id assignee_ids add_label_ids remove_label_ids subscription_event) + attrs = %i(state_event milestone_id add_label_ids remove_label_ids subscription_event) - if type == 'issue' + issuable_specific_attrs(type, attrs) + end + + def issuable_specific_attrs(type, attrs) + if type == 'issue' || type == 'merge_request' attrs.push(:assignee_ids) else attrs.push(:assignee_id) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index ae512563585..66f83d5c127 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -68,10 +68,10 @@ class NotificationService # Notify a user when a previously unknown IP or device is used to # sign in to their account - def unknown_sign_in(user, ip) + def unknown_sign_in(user, ip, time) return unless user.can?(:receive_notifications) - mailer.unknown_sign_in_email(user, ip).deliver_later + mailer.unknown_sign_in_email(user, ip, time).deliver_later end # When create an issue we should send an email to: diff --git a/app/views/notify/unknown_sign_in_email.html.haml b/app/views/notify/unknown_sign_in_email.html.haml index a4123fada1b..914242da5c6 100644 --- a/app/views/notify/unknown_sign_in_email.html.haml +++ b/app/views/notify/unknown_sign_in_email.html.haml @@ -1,14 +1,54 @@ -%p - = _('Hi %{username}!') % { username: sanitize_name(@user.name) } -%p - = _('A sign-in to your account has been made from the following IP address: %{ip}.') % { ip: @ip } -%p - - password_link_start = ''.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' } - = _('If you recently signed in and recognize the IP address, you may disregard this email.') - = _('If you did not recently sign in, you should immediately %{password_link_start}change your password%{password_link_end}.').html_safe % { password_link_start: password_link_start, password_link_end: ''.html_safe } - = _('Passwords should be unique and not used for any other sites or services.') +- default_font = "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" +- default_style = "#{default_font}font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" +- spacer_style = "#{default_font};height:18px;font-size:18px;line-height:18px;" -- unless @user.two_factor_enabled? - %p - - mfa_link_start = ''.html_safe - = _('To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method.').html_safe % { mfa_link_start: mfa_link_start, mfa_link_end: ''.html_safe } +%tr.alert + %td{ style: "#{default_font}padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#FC6D26;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "#{default_font}vertical-align:middle;color:#ffffff;text-align:center;" } + %span + = _("Your %{host} account was signed in to from a new location") % { host: Gitlab.config.gitlab.host } +%tr.spacer + %td{ style: spacer_style } +   +%tr.section + %td{ style: "#{default_font};padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %tbody + %tr + %td{ style: default_style } + = _('Hostname') + %td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;" } + = Gitlab.config.gitlab.host + %tr + %td{ style: "#{default_style}border-top:1px solid #ededed;" } + = _('IP Address') + %td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %span.muted{ style: "color:#333333;text-decoration:none;" } + = @ip + %tr + %td{ style: "#{default_style}border-top:1px solid #ededed;" } + = _('Time') + %td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + = @time.strftime('%Y-%m-%d %l:%M:%S %p %Z') +%tr.spacer + %td{ style: spacer_style } +   +%tr.section + %td{ style: "#{default_font};line-height:1.4;text-align:center;padding:0 15px;overflow:hidden;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" } + %tbody + %tr{ style: 'width:100%;' } + %td{ style: "#{default_style}text-align:center;" } + - password_link_start = ''.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' } + = _('If you recently signed in and recognize the IP address, you may disregard this email.') + %p + = _('If you did not recently sign in, you should immediately %{password_link_start}change your password%{password_link_end}.').html_safe % { password_link_start: password_link_start, password_link_end: ''.html_safe } + = _('Passwords should be unique and not used for any other sites or services.') + + - unless @user.two_factor_enabled? + %p + - mfa_link_start = ''.html_safe + = _('To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method.').html_safe % { mfa_link_start: mfa_link_start, mfa_link_end: ''.html_safe } diff --git a/cable/config.ru b/cable/config.ru index a528672ce25..c50bc41511d 100644 --- a/cable/config.ru +++ b/cable/config.ru @@ -5,4 +5,6 @@ Rails.application.eager_load! ACTION_CABLE_SERVER = true +use ActionDispatch::RequestId + run ActionCable.server diff --git a/changelogs/unreleased/dblessing-new-sign-in-email-beautification.yml b/changelogs/unreleased/dblessing-new-sign-in-email-beautification.yml new file mode 100644 index 00000000000..96ad1bc440d --- /dev/null +++ b/changelogs/unreleased/dblessing-new-sign-in-email-beautification.yml @@ -0,0 +1,5 @@ +--- +title: Improve new/unknown sign-in email styling +merge_request: 32808 +author: +type: changed diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index e1e15d1870c..01353ad4ec1 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -12,9 +12,9 @@ unless Gitlab::Runtime.sidekiq? config.lograge.logger = ActiveSupport::Logger.new(filename) config.lograge.before_format = lambda do |data, payload| data.delete(:error) - data[:db_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:db)) - data[:view_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:view)) - data[:duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:duration)) + data[:db_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:db)) if data[:db] + data[:view_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:view)) if data[:view] + data[:duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:duration)) if data[:duration] data end diff --git a/doc/administration/logs.md b/doc/administration/logs.md index eea1bc232e4..db21efe92a3 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -85,6 +85,29 @@ which correspond to: 1. `elasticsearch_calls`: total number of calls to Elasticsearch 1. `elasticsearch_duration_s`: total time taken by Elasticsearch calls +ActionCable connection and subscription events are also logged to this file and they follow the same +format above. The `method`, `path`, and `format` fields are not applicable, and are always empty. +The ActionCable connection or channel class is used as the `controller`. + +```json +{ + "method":{}, + "path":{}, + "format":{}, + "controller":"IssuesChannel", + "action":"subscribe", + "status":200, + "time":"2020-05-14T19:46:22.008Z", + "params":[{"key":"project_path","value":"gitlab/gitlab-foss"},{"key":"iid","value":"1"}], + "remote_ip":"127.0.0.1", + "user_id":1, + "username":"admin", + "ua":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:76.0) Gecko/20100101 Firefox/76.0", + "correlation_id":"jSOIEynHCUa", + "duration_s":0.32566 +} +``` + NOTE: **Note:** Starting with GitLab 12.5, if an error occurs, an `exception` field is included with `class`, `message`, and `backtrace`. Previous versions included an `error` field instead of diff --git a/doc/api/groups.md b/doc/api/groups.md index 51842f07ca4..2fe86758185 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -578,6 +578,18 @@ Additional response parameters: } ``` +Users on GitLab [Silver, Premium, or higher](https://about.gitlab.com/pricing/) will also see +the `marked_for_deletion_on` attribute: + +```json +{ + "id": 4, + "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", + "marked_for_deletion_on": "2020-04-03", + ... +} +``` + When adding the parameter `with_projects=false`, projects will not be returned. ```shell diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index f9ce611e778..b365c1a5b77 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -20,6 +20,13 @@ that can be reused in different scripts. Variables are useful for customizing your jobs in GitLab CI/CD. When you use variables, you don't have to hard-code values. +For more information about advanced use of GitLab CI/CD: + +- Get to productivity faster with these [7 advanced GitLab CI workflow hacks](https://about.gitlab.com/webcast/7cicd-hacks/) + shared by GitLab engineers. +- Learn how the Cloud Native Computing Foundation (CNCF) [eliminates the complexity](https://about.gitlab.com/customers/cncf/) + of managing projects across many cloud providers with GitLab CI/CD. + ## Predefined environment variables GitLab CI/CD has a [default set of predefined variables](predefined_variables.md) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 89a4b662881..9ac74998e35 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -25,6 +25,14 @@ We have complete examples of configuring pipelines: - For a collection of examples, see [GitLab CI/CD Examples](../examples/README.md). - To see a large `.gitlab-ci.yml` file used in an enterprise, see the [`.gitlab-ci.yml` file for `gitlab`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab-ci.yml). +For some additional information about GitLab CI/CD: + +- Watch the [CI/CD Ease of configuration](https://www.youtube.com/embed/opdLqwz6tcE) video. +- Watch the [Making the case for CI/CD in your organization](https://about.gitlab.com/compare/github-actions-alternative/) + webcast to learn the benefits of CI/CD and how to measure the results of CI/CD automation. +- Learn how [Verizon reduced rebuilds](https://about.gitlab.com/blog/2019/02/14/verizon-customer-story/) + from 30 days to under 8 hours with GitLab. + NOTE: **Note:** If you have a [mirrored repository where GitLab pulls from](../../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository-starter), you may need to enable pipeline triggering in your project's diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md index af36367fd2b..fa11da62363 100644 --- a/doc/development/fe_guide/graphql.md +++ b/doc/development/fe_guide/graphql.md @@ -1,7 +1,69 @@ # GraphQL +## Getting Started + +### Helpful Resources + +**General resources**: + +- [📚 Official Introduction to GraphQL](https://graphql.org/learn/) +- [📚 Official Introduction to Apollo](https://www.apollographql.com/docs/tutorial/introduction/) + +**GraphQL at GitLab**: + +- [🎬 GitLab Unfiltered GraphQL playlist](https://www.youtube.com/watch?v=wHPKZBDMfxE&list=PL05JrBw4t0KpcjeHjaRMB7IGB2oDWyJzv) +- [🎬 GraphQL at GitLab: Deep Dive](../api_graphql_styleguide.md#deep-dive) (video) by Nick Thomas + - An overview of the history of GraphQL at GitLab (not frontend-specific) +- [🎬 GitLab Feature Walkthrough with GraphQL and Vue Apollo](https://www.youtube.com/watch?v=6yYp2zB7FrM) (video) by Natalia Tepluhina + - A real-life example of implmenting a frontend feature in GitLab using GraphQL +- [🎬 History of client-side GraphQL at GitLab](https://www.youtube.com/watch?v=mCKRJxvMnf0) (video) Illya Klymov and Natalia Tepluhina +- [🎬 From Vuex to Apollo](https://www.youtube.com/watch?v=9knwu87IfU8) (video) by Natalia Tepluhina + - A useful overview of when Apollo might be a better choice than Vuex, and how one could go about the transition +- [🛠 Vuex-> Apollo Migration: a proof-of-concept project](https://gitlab.com/ntepluhina/vuex-to-apollo/blob/master/README.md) + - A collection of examples that show the possible approaches for state management with Vue+GraphQL+(Vuex or Apollo) apps + +### Libraries + +We use [Apollo](https://www.apollographql.com/) (specifically [Apollo Client](https://www.apollographql.com/docs/react/)) and [Vue Apollo](https://github.com/Akryum/vue-apollo/) +when using GraphQL for frontend development. + +If you are using GraphQL within a Vue application, the [Usage in Vue](#usage-in-vue) section +can help you learn how to integrate Vue Apollo. + +For other usecases, check out the [Usage outside of Vue](#usage-outside-of-vue) section. + +### Tooling + +- [Apollo Client Devtools](https://github.com/apollographql/apollo-client-devtools) + +#### [Apollo GraphQL VS Code extension](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) + +If you use VS Code, the Apollo GraphQL extension supports autocompletion in `.graphql` files. To set up +the GraphQL extension, follow these steps: + +1. Add an `apollo.config.js` file to the root of your `gitlab` local directory. +1. Populate the file with the following content: + + ```javascript + module.exports = { + client: { + includes: ['./app/assets/javascripts/**/*.graphql', './ee/app/assets/javascripts/**/*.graphql'], + service: { + name: 'GitLab', + localSchemaFile: './doc/api/graphql/reference/gitlab_schema.graphql', + }, + }, + }; + ``` + +1. Restart VS Code. + +### Exploring the GraphQL API + Our GraphQL API can be explored via GraphiQL at your instance's -`/-/graphql-explorer` or at [GitLab.com](https://gitlab.com/-/graphql-explorer). +`/-/graphql-explorer` or at [GitLab.com](https://gitlab.com/-/graphql-explorer). Consult the +[GitLab GraphQL API Reference documentation](../../api/graphql/reference) +where needed. You can check all existing queries and mutations on the right side of GraphiQL in its **Documentation explorer**. It's also possible to @@ -10,9 +72,6 @@ their execution by clicking **Execute query** button on the top left: ![GraphiQL interface](img/graphiql_explorer_v12_4.png) -We use [Apollo](https://www.apollographql.com/) and [Vue Apollo](https://github.com/vuejs/vue-apollo) for working with GraphQL -on the frontend. - ## Apollo Client To save duplicated clients getting created in different apps, we have a @@ -41,7 +100,7 @@ To distinguish queries from mutations and fragments, the following naming conven ### Fragments -Fragments are a way to make your complex GraphQL queries more readable and re-usable. Here is an example of GraphQL fragment: +[Fragments](https://graphql.org/learn/queries/#fragments) are a way to make your complex GraphQL queries more readable and re-usable. Here is an example of GraphQL fragment: ```javascript fragment DesignListItem on Design { @@ -210,7 +269,7 @@ Read more about local state management with Apollo in the [Vue Apollo documentat ### Using with Vuex -When Apollo Client is used within Vuex and fetched data is stored in the Vuex store, there is no need in keeping Apollo Client cache enabled. Otherwise we would have data from the API stored in two places - Vuex store and Apollo Client cache. More to say, with Apollo default settings, a subsequent fetch from the GraphQL API could result in fetching data from Apollo cache (in the case where we have the same query and variables). To prevent this behavior, we need to disable Apollo Client cache passing a valid `fetchPolicy` option to its constructor: +When Apollo Client is used within Vuex and fetched data is stored in the Vuex store, there is no need in keeping Apollo Client cache enabled. Otherwise we would have data from the API stored in two places - Vuex store and Apollo Client cache. More to say, with Apollo's default settings, a subsequent fetch from the GraphQL API could result in fetching data from Apollo cache (in the case where we have the same query and variables). To prevent this behavior, we need to disable Apollo Client cache passing a valid `fetchPolicy` option to its constructor: ```javascript import fetchPolicies from '~/graphql_shared/fetch_policy_constants'; @@ -587,4 +646,20 @@ defaultClient.query({ query }) .then(result => console.log(result)); ``` -Read more about the [Apollo](https://www.apollographql.com/) client in the [Apollo documentation](https://www.apollographql.com/docs/tutorial/client/). +When [using Vuex](#Using-with-Vuex), disable the cache when: + +- The data is being cached elsewhere +- The use case does not need caching +if the data is being cached elsewhere, or if there is simply no need for it for the given usecase. + +```javascript +import createDefaultClient from '~/lib/graphql'; +import fetchPolicies from '~/graphql_shared/fetch_policy_constants'; + +const defaultClient = createDefaultClient( + {}, + { + fetchPolicy: fetchPolicies.NO_CACHE, + }, +); +``` diff --git a/doc/development/go_guide/dependencies.md b/doc/development/go_guide/dependencies.md index a65e91869e3..b85344635c6 100644 --- a/doc/development/go_guide/dependencies.md +++ b/doc/development/go_guide/dependencies.md @@ -18,10 +18,10 @@ Prior to this, Go did not have any well-defined mechanism for version management While 3rd party version management tools existed, the default Go experience had no support for versioning. -Go modules use semantic versioning. The versions of a module are defined as VCS -tags that are valid semantic versions prefixed with `v`. For example, to release -version `1.0.0` of `gitlab.com/my/project`, the developer must create the Git -tag `v1.0.0`. +Go modules use [semantic versioning](https://semver.org). The versions of a +module are defined as VCS (version control system) tags that are valid semantic +versions prefixed with `v`. For example, to release version `1.0.0` of +`gitlab.com/my/project`, the developer must create the Git tag `v1.0.0`. For major versions other than 0 and 1, the module name must be suffixed with `/vX` where X is the major version. For example, version `v2.0.0` of @@ -38,6 +38,10 @@ end with a timestamp and the first 12 characters of the commit identifier: If a VCS tag matches one of these patterns, it is ignored. +For a complete understanding of Go modules and versioning, see [this series of +blog posts](https://blog.golang.org/using-go-modules) on the official Go +website. + ## 'Module' vs 'Package' - A package is a folder containing `*.go` files. diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index 837da349f7e..52be7451b1e 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -62,6 +62,7 @@ are very appreciative of the work done by translators and proofreaders! - Hiroyuki Sato - [GitLab](https://gitlab.com/hiroponz), [CrowdIn](https://crowdin.com/profile/hiroponz) - Tomo Dote - [GitLab](https://gitlab.com/fu7mu4), [CrowdIn](https://crowdin.com/profile/fu7mu4) - Hiromi Nozawa - [GitLab](https://gitlab.com/hir0mi), [CrowdIn](https://crowdin.com/profile/hir0mi) + - Takuya Noguchi - [GitLab](https://gitlab.com/tnir), [CrowdIn](https://crowdin.com/profile/tnir) - Korean - Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [CrowdIn](https://crowdin.com/profile/zzazang) - Ji Hun Oh - [GitLab](https://gitlab.com/Baw-Appie), [CrowdIn](https://crowdin.com/profile/BawAppie) diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md index 7d87630bbf3..2d1369c061b 100644 --- a/doc/user/packages/go_proxy/index.md +++ b/doc/user/packages/go_proxy/index.md @@ -54,7 +54,40 @@ NOTE: **Note:** GitLab does not currently display Go modules in the **Packages Registry** of a project. Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/213770) for details. -### Fetch modules from private projects +## Add GitLab as a Go proxy + +NOTE: **Note:** +To use a Go proxy, you must be using Go 1.13 or later. + +The available proxy endpoints are: + +- Project - can fetch modules defined by a project - `/api/v4/projects/:id/packages/go` + +To use the Go proxy for GitLab to fetch Go modules from GitLab, add the +appropriate proxy endpoint to `GOPROXY`. For details on setting Go environment +variables, see [Set environment variables](#set-environment-variables). For +details on configuring `GOPROXY`, see [Dependency Management in Go > +Proxies](../../../development/go_guide/dependencies.md#proxies). + +For example, adding the project-specific endpoint to `GOPROXY` will tell Go +to initially query that endpoint and fall back to the default behavior: + +```shell +go env -w GOPROXY='https://gitlab.com/api/v4/projects/1234/packages/go,https://proxy.golang.org,direct' +``` + +With this configuration, Go fetches dependencies as follows: + +1. Attempt to fetch from the project-specific Go proxy. +1. Attempt to fetch from [proxy.golang.org](https://proxy.golang.org). +1. Fetch directly with version control system operations (such as `git clone`, + `svn checkout`, and so on). + +If `GOPROXY` is not specified, Go follows steps 2 and 3, which corresponds to +setting `GOPROXY` to `https://proxy.golang.org,direct`. If `GOPROXY` only +contains the project-specific endpoint, Go will only query that endpoint. + +## Fetch modules from private projects `go` does not support transmitting credentials over insecure connections. The steps below work only if GitLab is configured for HTTPS. @@ -64,7 +97,7 @@ steps below work only if GitLab is configured for HTTPS. 1. Configure Go to skip downloading of checksums for private GitLab projects from the public checksum database. -#### Enable Request Authentication +### Enable request authentication Create a [personal access token](../../profile/personal_access_tokens.md) with the `api` or `read_api` scope and add it to @@ -78,90 +111,53 @@ machine login password `` and `` should be your username and the personal access token, respectively. -#### Disable checksum database queries +### Disable checksum database queries -Go can be configured to query a checksum database for module checksums. Go 1.13 -and later query `sum.golang.org` by default. This fails for modules that are not -public and thus not accessible to `sum.golang.org`. To resolve this issue, set -`GONOSUMDB` to a comma-separated list of projects or namespaces for which Go -should not query the checksum database. For example, `go env -w -GONOSUMDB=gitlab.com/my/project` persistently configures Go to skip checksum -queries for the project `gitlab.com/my/project`. +When downloading dependencies, by default Go 1.13 and later validate fetched +sources against the checksum database `sum.golang.org`. If the checksum of the +fetched sources does not match the checksum from the database, Go will not build +the dependency. This causes private modules to fail to build, as +`sum.golang.org` cannot fetch the source of private modules and thus cannot +provide a checksum. To resolve this issue, `GONOSUMDB` should be set to a +comma-separated list of private projects. For details on setting Go environment +variables, see [Set environment variables](#set-environment-variables). For more +details on disabling this feature of Go, see [Dependency Management in Go > +Checksums](../../../development/go_guide/dependencies.md#checksums). -Checksum database queries can be disabled for arbitrary prefixes or disabled -entirely. However, checksum database queries are a security mechanism and as -such they should be disabled selectively and only when necessary. `GOSUMDB=off` -or `GONOSUMDB=*` disables checksum queries entirely. `GONOSUMDB=gitlab.com` -disables checksum queries for all projects hosted on GitLab.com. - -## Add GitLab as a Go proxy - -NOTE: **Note:** -To use a Go proxy, you must be using Go 1.13 or later. - -The available proxy endpoints are: - -- Project - can fetch modules defined by a project - `/api/v4/projects/:id/packages/go` - -Go's use of proxies is configured with the `GOPROXY` environment variable, as a -comma separated list of URLs. Go 1.14 adds support for comma separated list of -URLs. Go 1.14 adds support for using `go env -w` to manage Go's environment -variables. For example, `go env -w GOPROXY=...` writes to `$GOPATH/env` -(which defaults to `~/.go/env`). `GOPROXY` can also be configured as a normal -environment variable, with RC files or `export GOPROXY=...`. - -The default value of `$GOPROXY` is `https://proxy.golang.org,direct`, which -tells `go` to first query `proxy.golang.org` and fallback to direct VCS -operations (`git clone`, `svc checkout`, etc). Replacing -`https://proxy.golang.org` with a GitLab endpoint will direct all fetches -through GitLab. Currently GitLab's Go proxy does not support dependency -proxying, so all external dependencies will be handled directly. If GitLab's -endpoint is inserted before `https://proxy.golang.org`, then all fetches will -first go through GitLab. This can help avoid making requests for private -packages to the public proxy, but `GOPRIVATE` is a much safer way of achieving -that. - -For example, with the following configuration, Go will attempt to fetch modules -from 1) GitLab project 1234's Go module proxy, 2) `proxy.golang.org`, and -finally 3) directly with Git (or another VCS, depending on where the module -source is hosted). +For example, to disable checksum queries for `gitlab.com/my/project`, set `GONOSUMDB`: ```shell -go env -w GOPROXY=https://gitlab.com/api/v4/projects/1234/packages/go,https://proxy.golang.org,direct +go env -w GONOSUMDB='gitlab.com/my/project,' ``` -## Release a module +## Working with Go -Go modules and module versions are handled entirely with Git (or SVN, Mercurial, -and so on). A module is a repository containing Go source and a `go.mod` file. A -version of a module is a Git tag (or equivalent) that is a valid [semantic -version](https://semver.org), prefixed with 'v'. For example, `v1.0.0` and -`v1.3.2-alpha` are valid module versions, but `v1` or `v1.2` are not. +If you are unfamiliar with managing dependencies in Go, or Go in general, +consider reviewing the following documentation: -Go requires that major versions after v1 involve a change in the import path of -the module. For example, version 2 of the module `gitlab.com/my/project` must be -imported and released as `gitlab.com/my/project/v2`. +- [Dependency Management in Go](../../../development/go_guide/dependencies.md) +- [Go Modules Reference](https://golang.org/ref/mod) +- [Documentation (golang.org)](https://golang.org/doc/) +- [Learn (learn.go.dev)](https://learn.go.dev/) -For a complete understanding of Go modules and versioning, see [this series of -blog posts](https://blog.golang.org/using-go-modules) on the official Go -website. +### Set environment variables -## Valid modules and versions +Go uses environment variables to control various features. These can be managed +in all the usual ways, but Go 1.14 will read and write Go environment variables +from and to a special Go environment file, `~/.go/env` by default. If `GOENV` is +set to a file, Go will read and write that file instead. If `GOENV` is not set +but `GOPATH` is set, Go will read and write `$GOPATH/env`. -The GitLab Go proxy will ignore modules and module versions that have an invalid -`module` directive in their `go.mod`. Go requires that a package imported as -`gitlab.com/my/project` can be accessed with that same URL, and that the first -line of `go.mod` is `module gitlab.com/my/project`. If `go.mod` names a -different module, compilation will fail. Additionally, Go requires, for major -versions after 1, that the name of the module have an appropriate suffix, for -example `gitlab.com/my/project/v2`. If the `module` directive does not also have -this suffix, compilation will fail. +Go environment variables can be read with `go env ` and, in Go 1.14 and +later, can be written with `go env -w =`. For example, `go env +GOPATH` or `go env -w GOPATH=/go`. -Go supports 'pseudo-versions' that encode the timestamp and SHA of a commit. -Tags that match the pseudo-version pattern are ignored, as otherwise they could -interfere with fetching specific commits using a pseudo-version. Pseudo-versions -follow one of three formats: +### Release a module -- `vX.0.0-yyyymmddhhmmss-abcdefabcdef`, when no earlier tagged commit exists for X. -- `vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef`, when most recent prior tag is vX.Y.Z-pre. -- `vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef`, when most recent prior tag is vX.Y.Z. +Go modules and module versions are defined by source repositories, such as Git, +SVN, Mercurial, and so on. A module is a repository containing `go.mod` and Go +files. Module versions are defined by VCS tags. To publish a module, push +`go.mod` and source files to a VCS repository. To publish a module version, push +a VCS tag. See [Dependency Management in Go > +Versioning](../../../development/go_guide/dependencies.md#versioning) for more +details on what constitutes a valid module or module version. diff --git a/doc/user/profile/img/unknown_sign_in_email_v13_1.png b/doc/user/profile/img/unknown_sign_in_email_v13_1.png new file mode 100644 index 0000000000000000000000000000000000000000..586be483be95dbc8e60fc3668e92f71f6c49bfa2 GIT binary patch literal 20230 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 z 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 literal 0 HcmV?d00001 diff --git a/doc/user/profile/unknown_sign_in_notification.md b/doc/user/profile/unknown_sign_in_notification.md index 9400ead1922..aa3efaa38bb 100644 --- a/doc/user/profile/unknown_sign_in_notification.md +++ b/doc/user/profile/unknown_sign_in_notification.md @@ -1,5 +1,7 @@ # Email notification for unknown sign-ins +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0. + When a user successfully signs in from a previously unknown IP address, GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially malicious or unauthorized sign-ins. @@ -13,4 +15,4 @@ There are two methods used to identify a known sign-in: ## Example email -![Unknown sign in email](./img/unknown_sign_in_email_v13_0.png) +![Unknown sign in email](./img/unknown_sign_in_email_v13_1.png) diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md index 23cfc017179..773d2ff009d 100644 --- a/doc/user/project/releases/index.md +++ b/doc/user/project/releases/index.md @@ -67,6 +67,8 @@ A link is any URL which can point to whatever you like; documentation, built binaries, or other related materials. These can be both internal or external links from your GitLab instance. +The four types of links are "Runbook," "Package," "Image," and "Other." + #### Permanent links to Release assets > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27300) in GitLab 12.9. diff --git a/lib/gitlab/lograge/custom_options.rb b/lib/gitlab/lograge/custom_options.rb index 55c46c365f6..70a26686424 100644 --- a/lib/gitlab/lograge/custom_options.rb +++ b/lib/gitlab/lograge/custom_options.rb @@ -19,17 +19,17 @@ module Gitlab remote_ip: event.payload[:remote_ip], user_id: event.payload[:user_id], username: event.payload[:username], - ua: event.payload[:ua], - queue_duration_s: event.payload[:queue_duration_s] + ua: event.payload[:ua] } payload.merge!(event.payload[:metadata]) if event.payload[:metadata] ::Gitlab::InstrumentationHelper.add_instrumentation_data(payload) + payload[:queue_duration_s] = event.payload[:queue_duration_s] if event.payload[:queue_duration_s] payload[:response] = event.payload[:response] if event.payload[:response] payload[:etag_route] = event.payload[:etag_route] if event.payload[:etag_route] - payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id + payload[Labkit::Correlation::CorrelationId::LOG_KEY] = event.payload[Labkit::Correlation::CorrelationId::LOG_KEY] || Labkit::Correlation::CorrelationId.current_id if cpu_s = Gitlab::Metrics::System.thread_cpu_duration(::Gitlab::RequestContext.instance.start_thread_cpu_time) payload[:cpu_s] = cpu_s.round(2) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d67394f99a4..87590fca2c4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -358,6 +358,9 @@ msgstr "" msgid "%{group_name} uses group managed accounts. You need to create a new GitLab account which will be managed by %{group_name}." msgstr "" +msgid "%{host} sign-in from new location" +msgstr "" + msgid "%{icon}You are about to add %{usersTag} people to the discussion. Proceed with caution." msgstr "" @@ -968,9 +971,6 @@ msgstr "" msgid "A sign-in to your account has been made from the following IP address: %{ip}" msgstr "" -msgid "A sign-in to your account has been made from the following IP address: %{ip}." -msgstr "" - msgid "A subscription will trigger a new pipeline on the default branch of this project when a pipeline successfully completes for a new tag on the %{default_branch_docs} of the subscribed project." msgstr "" @@ -11405,6 +11405,9 @@ msgstr "" msgid "Hook was successfully updated." msgstr "" +msgid "Hostname" +msgstr "" + msgid "Hour (UTC)" msgstr "" @@ -23531,9 +23534,6 @@ msgstr "" msgid "Unknown response text" msgstr "" -msgid "Unknown sign-in from new location" -msgstr "" - msgid "Unlimited" msgstr "" @@ -25535,6 +25535,9 @@ msgstr "" msgid "YouTube" msgstr "" +msgid "Your %{host} account was signed in to from a new location" +msgstr "" + msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}." msgstr "" diff --git a/spec/channels/issues_channel_spec.rb b/spec/channels/issues_channel_spec.rb index 1c88cc73456..d87541cad46 100644 --- a/spec/channels/issues_channel_spec.rb +++ b/spec/channels/issues_channel_spec.rb @@ -18,7 +18,7 @@ describe IssuesChannel do end it 'rejects when the user does not have access' do - stub_connection current_user: nil + stub_action_cable_connection current_user: nil subscribe(project_path: issue.project.full_path, iid: issue.iid) @@ -26,7 +26,7 @@ describe IssuesChannel do end it 'subscribes to a stream when the user has access' do - stub_connection current_user: issue.author + stub_action_cable_connection current_user: issue.author subscribe(project_path: issue.project.full_path, iid: issue.iid) diff --git a/spec/features/action_cable_logging_spec.rb b/spec/features/action_cable_logging_spec.rb new file mode 100644 index 00000000000..f0bdb5fdd8c --- /dev/null +++ b/spec/features/action_cable_logging_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'ActionCable logging', :js do + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:user) { create(:user) } + + before_all do + project.add_developer(user) + end + + it 'adds extra context to logs' do + allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original + + expect(ActiveSupport::Notifications).to receive(:instrument).with( + 'connect.action_cable', + a_hash_including(remote_ip: '127.0.0.1', user_id: nil, username: nil) + ) + + subscription_data = a_hash_including( + remote_ip: '127.0.0.1', + user_id: user.id, + username: user.username, + params: a_hash_including( + project_path: project.full_path, + iid: issue.iid.to_s + ) + ) + + expect(ActiveSupport::Notifications).to receive(:instrument).with('subscribe.action_cable', subscription_data) + + gitlab_sign_in(user) + visit project_issue_path(project, issue) + end +end diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb index c243217d2a2..f283ac100a9 100644 --- a/spec/initializers/lograge_spec.rb +++ b/spec/initializers/lograge_spec.rb @@ -99,7 +99,7 @@ describe 'lograge', type: :request do end context 'with a log subscriber' do - let(:subscriber) { Lograge::RequestLogSubscriber.new } + let(:subscriber) { Lograge::LogSubscribers::ActionController.new } let(:event) do ActiveSupport::Notifications::Event.new( diff --git a/spec/lib/gitlab/lograge/custom_options_spec.rb b/spec/lib/gitlab/lograge/custom_options_spec.rb index 7ae8baa31b5..f261f88ebfc 100644 --- a/spec/lib/gitlab/lograge/custom_options_spec.rb +++ b/spec/lib/gitlab/lograge/custom_options_spec.rb @@ -13,21 +13,16 @@ describe Gitlab::Lograge::CustomOptions do } end - let(:event) do - ActiveSupport::Notifications::Event.new( - 'test', - 1, - 2, - 'transaction_id', - { - params: params, - user_id: 'test', - cf_ray: SecureRandom.hex, - cf_request_id: SecureRandom.hex, - metadata: { 'meta.user' => 'jane.doe' } - } - ) + let(:event_payload) do + { + params: params, + user_id: 'test', + cf_ray: SecureRandom.hex, + cf_request_id: SecureRandom.hex, + metadata: { 'meta.user' => 'jane.doe' } + } end + let(:event) { ActiveSupport::Notifications::Event.new('test', 1, 2, 'transaction_id', event_payload) } subject { described_class.call(event) } @@ -63,19 +58,23 @@ describe Gitlab::Lograge::CustomOptions do end context 'when metadata is missing' do - let(:event) do - ActiveSupport::Notifications::Event.new( - 'test', - 1, - 2, - 'transaction_id', - { params: {} } - ) - end + let(:event_payload) { { params: {} } } it 'does not break' do expect { subject }.not_to raise_error end end + + context 'when correlation_id is overriden' do + let(:correlation_id_key) { Labkit::Correlation::CorrelationId::LOG_KEY } + + before do + event_payload[correlation_id_key] = '123456' + end + + it 'sets the overriden value' do + expect(subject[correlation_id_key]).to eq('123456') + end + end end end diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb index f84bf43b9c4..cbf42da2085 100644 --- a/spec/mailers/emails/profile_spec.rb +++ b/spec/mailers/emails/profile_spec.rb @@ -160,38 +160,48 @@ describe Emails::Profile do describe 'user unknown sign in email' do let_it_be(:user) { create(:user) } let_it_be(:ip) { '169.0.0.1' } + let_it_be(:current_time) { Time.current } + let_it_be(:email) { Notify.unknown_sign_in_email(user, ip, current_time) } - subject { Notify.unknown_sign_in_email(user, ip) } + subject { email } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'a user cannot unsubscribe through footer link' it 'is sent to the user' do - expect(subject).to deliver_to user.email + is_expected.to deliver_to user.email end it 'has the correct subject' do - expect(subject).to have_subject /^Unknown sign-in from new location$/ + is_expected.to have_subject "#{Gitlab.config.gitlab.host} sign-in from new location" end - it 'mentions the unknown sign-in IP' do - expect(subject).to have_body_text /A sign-in to your account has been made from the following IP address: #{ip}./ + it 'mentions the new sign-in IP' do + is_expected.to have_body_text ip end - it 'includes a link to the change password page' do - expect(subject).to have_body_text /#{edit_profile_password_path}/ + it 'mentioned the time' do + is_expected.to have_body_text current_time.strftime('%Y-%m-%d %l:%M:%S %p %Z') + end + + it 'includes a link to the change password documentation' do + is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' end it 'mentions two factor authentication when two factor is not enabled' do - expect(subject).to have_body_text /two-factor authentication/ + is_expected.to have_body_text 'two-factor authentication' + end + + it 'includes a link to two-factor authentication documentation' do + is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html' end context 'when two factor authentication is enabled' do - it 'does not mention two factor authentication' do - two_factor_user = create(:user, :two_factor) + let(:user) { create(:user, :two_factor) } - expect( Notify.unknown_sign_in_email(two_factor_user, ip) ) + it 'does not mention two factor authentication' do + expect( Notify.unknown_sign_in_email(user, ip, current_time) ) .not_to have_body_text /two-factor authentication/ end end diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb index 6a97d91b3ca..24b47be3c69 100644 --- a/spec/models/active_session_spec.rb +++ b/spec/models/active_session_spec.rb @@ -16,7 +16,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do double(:request, { user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 ' \ '(KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]', - ip: '127.0.0.1', + remote_ip: '127.0.0.1', session: session }) end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 9943f2c01ca..3c1c3e2dfc3 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -243,11 +243,12 @@ describe NotificationService, :mailer do describe '#unknown_sign_in' do let_it_be(:user) { create(:user) } let_it_be(:ip) { '127.0.0.1' } + let_it_be(:time) { Time.current } - subject { notification.unknown_sign_in(user, ip) } + subject { notification.unknown_sign_in(user, ip, time) } it 'sends email to the user' do - expect { subject }.to have_enqueued_email(user, ip, mail: 'unknown_sign_in_email') + expect { subject }.to have_enqueued_email(user, ip, time, mail: 'unknown_sign_in_email') end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 46237c6c9c9..84de5119505 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -139,6 +139,7 @@ RSpec.configure do |config| config.include IdempotentWorkerHelper, type: :worker config.include RailsHelpers config.include SidekiqMiddleware + config.include StubActionCableConnection, type: :channel if ENV['CI'] || ENV['RETRIES'] # This includes the first try, i.e. tests will be run 4 times before failing. diff --git a/spec/support/action_cable.rb b/spec/support/action_cable.rb new file mode 100644 index 00000000000..64cfc435875 --- /dev/null +++ b/spec/support/action_cable.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:each, type: :channel) do + stub_action_cable_connection + end +end diff --git a/spec/support/helpers/stub_action_cable_connection.rb b/spec/support/helpers/stub_action_cable_connection.rb new file mode 100644 index 00000000000..b4e9c2ae48c --- /dev/null +++ b/spec/support/helpers/stub_action_cable_connection.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module StubActionCableConnection + def stub_action_cable_connection(current_user: nil, request: ActionDispatch::TestRequest.create) + stub_connection(current_user: current_user, request: request) + end +end