Commit ba37d7ec authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 67a2efce 2343c8e3
import $ from 'jquery';
import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { deprecatedCreateFlash as Flash } from '~/flash';
import Api from '~/api';
......@@ -6,7 +7,6 @@ import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import { visitUrl } from '~/lib/utils/url_utility';
import refreshCounts from './refresh_counts';
import setHighlightClass from './highlight_blob_search_result';
export default class Search {
constructor() {
......
......@@ -45,6 +45,10 @@ module Ci
def get_store_class(store)
@stores ||= {}
# Can't memoize this because the feature flag may alter this
return fog_store_class.new if store.to_sym == :fog
@stores[store] ||= "Ci::BuildTraceChunks::#{store.capitalize}".constantize.new
end
......@@ -74,6 +78,14 @@ module Ci
def metadata_attributes
attribute_names - %w[raw_data]
end
def fog_store_class
if Feature.enabled?(:ci_trace_new_fog_store)
Ci::BuildTraceChunks::Fog
else
Ci::BuildTraceChunks::LegacyFog
end
end
end
def data
......
......@@ -8,13 +8,17 @@ module Ci
end
def data(model)
connection.get_object(bucket_name, key(model))[:body]
files.get(key(model))&.body
rescue Excon::Error::NotFound
# If the object does not exist in the object storage, this method returns nil.
end
def set_data(model, new_data)
connection.put_object(bucket_name, key(model), new_data)
# TODO: Support AWS S3 server side encryption
files.create({
key: key(model),
body: new_data
})
end
def append_data(model, new_data, offset)
......@@ -43,7 +47,7 @@ module Ci
def delete_keys(keys)
keys.each do |key|
connection.delete_object(bucket_name, key_raw(*key))
files.destroy(key_raw(*key))
end
end
......@@ -69,6 +73,14 @@ module Ci
@connection ||= ::Fog::Storage.new(object_store.connection.to_hash.deep_symbolize_keys)
end
def fog_directory
@fog_directory ||= connection.directories.new(key: bucket_name)
end
def files
@files ||= fog_directory.files
end
def object_store
Gitlab.config.artifacts.object_store
end
......
# frozen_string_literal: true
module Ci
module BuildTraceChunks
class LegacyFog
def available?
object_store.enabled
end
def data(model)
connection.get_object(bucket_name, key(model))[:body]
rescue Excon::Error::NotFound
# If the object does not exist in the object storage, this method returns nil.
end
def set_data(model, new_data)
connection.put_object(bucket_name, key(model), new_data)
end
def append_data(model, new_data, offset)
if offset > 0
truncated_data = data(model).to_s.byteslice(0, offset)
new_data = truncated_data + new_data
end
set_data(model, new_data)
new_data.bytesize
end
def size(model)
data(model).to_s.bytesize
end
def delete_data(model)
delete_keys([[model.build_id, model.chunk_index]])
end
def keys(relation)
return [] unless available?
relation.pluck(:build_id, :chunk_index)
end
def delete_keys(keys)
keys.each do |key|
connection.delete_object(bucket_name, key_raw(*key))
end
end
private
def key(model)
key_raw(model.build_id, model.chunk_index)
end
def key_raw(build_id, chunk_index)
"tmp/builds/#{build_id.to_i}/chunks/#{chunk_index.to_i}.log"
end
def bucket_name
return unless available?
object_store.remote_directory
end
def connection
return unless available?
@connection ||= ::Fog::Storage.new(object_store.connection.to_hash.deep_symbolize_keys)
end
def object_store
Gitlab.config.artifacts.object_store
end
end
end
end
......@@ -16,8 +16,8 @@ module ProjectServicesLoggable
def build_message(message, params = {})
{
service_class: self.class.name,
project_id: project.id,
project_path: project.full_path,
project_id: project&.id,
project_path: project&.full_path,
message: message
}.merge(params)
end
......
......@@ -7,4 +7,4 @@
= search_blob_title(project, path)
- if blob.data
.file-content.code.term{ data: { qa_selector: 'file_text_content' } }
= render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
= render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link, highlight_line: blob.highlight_line
#blob-content.file-content.code.js-syntax-highlight
- offset = defined?(first_line_number) ? first_line_number : 1
.line-numbers
- if blob.data.present?
- link_icon = sprite_icon('link', size: 12)
- link = blob_link if defined?(blob_link)
- blob.data.each_line.each_with_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.diff-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i }
= link_icon
= i
.blob-content{ data: { blob_id: blob.id, path: blob.path } }
- highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil
.blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight } }
%pre.code.highlight
%code
= blob.present.highlight
---
title: Fix exception when saving Jira integration info for an instance
merge_request: 45718
author:
type: fixed
---
title: Allow user snippets to be indexed by search crawlers
merge_request: 45793
author:
type: changed
---
name: ci_trace_new_fog_store
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46209
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273405
type: development
group: group::testing
default_enabled: false
......@@ -27,16 +27,16 @@ be prompted to activate your U2F device (usually by pressing a button on it),
and it will perform secure authentication on your behalf.
It is highly recommended that you set up 2FA with both a
[one-time password authenticator](#enable-2fa-via-one-time-password-authenticator)
and a [U2F device](#enable-2fa-via-u2f-device), so you can still access your account
if you lose your U2F device.
[one-time password authenticator](#one-time-password) or use [FortiAuthenticator](#one-time-password-via-fortiauthenticator)
and a [U2F device](#u2f-device), so you can still access your account if you
lose your U2F device.
## Enabling 2FA
There are two ways to enable two-factor authentication: via a one time password authenticator
or a U2F device.
### Enable 2FA via one time password authenticator
### One-time password
To enable 2FA:
......@@ -66,7 +66,81 @@ two-factor authentication has been enabled, and you'll be presented with a list
of [recovery codes](#recovery-codes). Make sure you download them and keep them
in a safe place.
### Enable 2FA via U2F device
### One-time password via FortiAuthenticator
> - Introduced in [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/212312)
> - It's deployed behind a feature flag, disabled by default.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-fortiauthenticator-integration).
You can use FortiAuthenticator as an OTP provider in GitLab. Users must exist in
both FortiAuthenticator and GitLab with the exact same username, and users must
have FortiToken configured in FortiAuthenticator.
You'll also need a username and access token for FortiAuthenticator. The
`access_token` in the code samples shown below is the FortAuthenticator access
key. To get the token, see the `REST API Solution Guide` at
[`Fortinet Document Library`](https://docs.fortinet.com/document/fortiauthenticator/6.2.0/rest-api-solution-guide/158294/the-fortiauthenticator-api).
GitLab 13.5 has been tested with FortAuthenticator version 6.2.0.
First configure FortiAuthenticator in GitLab. On your GitLab server:
1. Open the configuration file.
For Omnibus GitLab:
```shell
sudo editor /etc/gitlab/gitlab.rb
```
For installations from source:
```shell
cd /home/git/gitlab
sudo -u git -H editor config/gitlab.yml
```
1. Add the provider configuration:
For Omnibus package:
```ruby
gitlab_rails['forti_authenticator_enabled'] = true
gitlab_rails['forti_authenticator_host'] = 'forti_authenticator.example.com'
gitlab_rails['forti_authenticator_port'] = 443
gitlab_rails['forti_authenticator_username'] = '<some_username>'
gitlab_rails['forti_authenticator_access_token'] = 's3cr3t'
```
For installations from source:
```yaml
forti_authenticator:
enabled: true
host: forti_authenticator.example.com
port: 443
username: <some_username>
access_token: s3cr3t
```
1. Save the configuration file.
1. [Reconfigure](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure)
or [restart GitLab](../../../administration/restart_gitlab.md#installations-from-source)
for the changes to take effect if you installed GitLab via Omnibus or from
source respectively.
#### Enable FortiAuthenticator integration
This feature comes with the `:forti_authenticator` feature flag disabled by
default.
To enable this feature, ask a GitLab administrator with [Rails console access](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags)
to run the following command:
```ruby
Feature.enable(:forti_authenticator, User.find(<user ID>))
```
### U2F device
> Introduced in [GitLab 8.9](https://about.gitlab.com/blog/2016/06/22/gitlab-adds-support-for-u2f/).
......
# Export Merge Requests to CSV **(CORE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3619) in GitLab 13.6.
> - It's [deployed behind a feature flag](../../../administration/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-export-merge-requests-to-csv). **(CORE ONLY)**
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
Exporting Merge Requests CSV enables you and your team to export all the data collected from merge requests into a comma-separated values (CSV) file, which stores tabular data in plain text.
To export Merge Requests to CSV, navigate to your **Merge Requests** from the sidebar of a project and click **Export to CSV**.
Exported files are generated asynchronously and delivered as an email attachment upon generation.
## CSV Output
The following table shows what attributes will be present in the CSV.
| Column | Description |
|--------------------|--------------------------------------------------------------|
| MR ID | MR iid |
| URL | A link to the merge request on GitLab |
| Title | Merge request title |
| State | Opened, Closed, Locked, or Merged |
| Description | Merge request description |
| Source Branch | Source branch |
| Target Branch | Target branch |
| Source Project ID | ID of the source project |
| Target Project ID | ID of the target project |
| Author | Full name of the merge request author |
| Author Username | Username of the author, with the @ symbol omitted |
| Assignees | Full names of the merge request assignees, joined with a `,` |
| Assignee Usernames | Username of the assignees, with the @ symbol omitted |
| Approvers | Full names of the approvers, joined with a `,` |
| Approver Usernames | Username of the approvers, with the @ symbol omitted |
| Merged User | Full name of the merged user |
| Merged Username | Username of the merge user, with the @ symbol omitted |
| Milestone ID | ID of the merge request milestone |
| Created At (UTC) | Formatted as YYYY-MM-DD HH:MM:SS |
| Updated At (UTC) | Formatted as YYYY-MM-DD HH:MM:SS |
## Limitations
- Export merge requests to CSV is not available at the Group’s merge request list.
- As the merge request CSV file is sent as an email attachment, the size is limited to 15MB to ensure successful delivery across a range of email providers. If you need to minimize the size of the file, you can narrow the search before export. For example, you can set up exports of open and closed merge requests in separate files.
### Enable or disable Export Merge Requests to CSV **(CORE ONLY)**
Export merge requests to CSV is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:export_merge_requests_as_csv)
```
To disable it:
```ruby
Feature.enable(:export_merge_requests_as_csv)
```
Optionally, pass a project as an argument to enable for a single project.
```ruby
Feature.enable(:export_merge_requests_as_csv, project)
```
......@@ -56,7 +56,9 @@ export default () => ({
value: null,
colClass: 'number',
popover: {
content: s__('SubscriptionTable|Usage count is performed once a day at 12:00 PM.'),
content: s__(
'SubscriptionTable|This is the number of seats you will be required to purchase if you update to a paid plan.',
),
},
},
{
......
import setHighlightClass from '~/search/highlight_blob_search_result';
export default () => {
const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body');
const blobs = contentBody.querySelectorAll('.blob-result');
// Supports Basic (backed by Gitaly) Search highlighting
setHighlightClass();
// Supports Advanced (backed by Elasticsearch) Search highlighting
blobs.forEach(blob => {
const lines = blob.querySelectorAll('.line');
const dataHighlightLine = blob.querySelector('[data-highlight-line]');
if (dataHighlightLine) {
const { highlightLine } = dataHighlightLine.dataset;
lines[highlightLine].classList.add(highlightLineClass);
}
});
};
......@@ -23,7 +23,7 @@ module EE
feature_category :code_review, [:delete_description_version, :description_diff]
feature_category :container_scanning, [:container_scanning_reports]
feature_category :dependency_scanning, [:dependency_scanning_reports]
feature_category :fuzz_testing, [:coverage_fuzzing_reports]
feature_category :fuzz_testing, [:coverage_fuzzing_reports, :api_fuzzing_reports]
feature_category :license_compliance, [:license_scanning_reports]
feature_category :static_application_security_testing, [:sast_reports]
feature_category :secret_detection, [:secret_detection_reports]
......
......@@ -248,6 +248,10 @@ module EE
compare_reports(::Ci::CompareSecurityReportsService, current_user, 'coverage_fuzzing')
end
def has_api_fuzzing_reports?
!!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.api_fuzzing_reports)
end
def compare_api_fuzzing_reports(current_user)
return missing_report_error('api fuzzing') unless has_api_fuzzing_reports?
......
......@@ -2,6 +2,7 @@
class GitlabSubscription < ApplicationRecord
include EachBatch
include Gitlab::Utils::StrongMemoize
default_value_for(:start_date) { Date.today }
before_update :log_previous_state_for_update
......@@ -90,8 +91,23 @@ class GitlabSubscription < ApplicationRecord
self.hosted_plan = Plan.find_by(name: code)
end
# We need to show seats in use for free or trial subscriptions
# in order to make it easy for customers to get this information.
def seats_in_use
return super unless Feature.enabled?(:seats_in_use_for_free_or_trial)
return super if has_a_paid_hosted_plan? || !hosted?
seats_in_use_now
end
private
def seats_in_use_now
strong_memoize(:seats_in_use_now) do
calculate_seats_in_use
end
end
def log_previous_state_for_update
attrs = self.attributes.merge(self.attributes_in_database)
log_previous_state_to_history(:gitlab_subscription_updated, attrs)
......
......@@ -9,7 +9,7 @@
- plans_data.each do |plan|
= render 'shared/billings/billing_plan', namespace: namespace, plan: plan, current_plan: current_plan
- if namespace.actual_plan&.paid?
- if namespace.gitlab_subscription&.has_a_paid_hosted_plan?
.center.gl-mb-7
&= s_('BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}.').html_safe % { support_link_start: support_link_start, support_link_end: support_link_end }
......
---
title: Fix quoted search term code highlighting
merge_request: 45278
author:
type: fixed
---
title: Return seats in use for free or trial subscriptions
merge_request: 44973
author:
type: changed
---
name: seats_in_use_for_free_or_trial
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44973
rollout_issue_url:
type: development
group: group::provision
default_enabled: false
......@@ -12,6 +12,7 @@ resources :merge_requests, only: [], constraints: { id: /\d+/ } do
get :secret_detection_reports
get :dast_reports
get :coverage_fuzzing_reports
get :api_fuzzing_reports
post :rebase
end
......
......@@ -141,10 +141,12 @@ module Gitlab
highlight_content = result.dig('highlight', 'blob.content')&.first || ''
found_line_number = 0
highlight_found = false
highlight_content.each_line.each_with_index do |line, index|
if line.include?(::Elastic::Latest::GitClassProxy::HIGHLIGHT_START_TAG)
found_line_number = index
highlight_found = true
break
end
end
......@@ -162,12 +164,15 @@ module Gitlab
end
data = content.lines[from..to]
# only send highlighted line number if a highlight was returned by Elasticsearch
highlight_line = highlight_found ? found_line_number + 1 : nil
::Gitlab::Search::FoundBlob.new(
path: path,
basename: basename,
ref: ref,
startline: from + 1,
highlight_line: highlight_line,
data: data.join,
project: project,
project_id: project_id
......
......@@ -659,6 +659,79 @@ RSpec.describe Projects::MergeRequestsController do
it_behaves_like 'authorize read pipeline'
end
describe 'GET #api_fuzzing_reports' do
let(:merge_request) { create(:ee_merge_request, :with_api_fuzzing_reports, source_project: project, author: create(:user)) }
let(:params) do
{
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid
}
end
subject { get :api_fuzzing_reports, params: params, format: :json }
before do
allow_any_instance_of(::MergeRequest).to receive(:compare_reports)
.with(::Ci::CompareSecurityReportsService, viewer, 'api_fuzzing').and_return(comparison_status)
end
it_behaves_like 'pending pipeline response'
context 'when comparison is being processed' do
let(:comparison_status) { { status: :parsing } }
it 'sends polling interval' do
expect(::Gitlab::PollingInterval).to receive(:set_header)
subject
end
it 'returns 204 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when comparison is done' do
let(:comparison_status) { { status: :parsed, data: { added: [], fixed: [], existing: [] } } }
it 'does not send polling interval' do
expect(::Gitlab::PollingInterval).not_to receive(:set_header)
subject
end
it 'returns 200 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ "added" => [], "fixed" => [], "existing" => [] })
end
end
context 'when user created corrupted fuzzing reports' do
let(:comparison_status) { { status: :error, status_reason: 'Failed to parse api fuzzing reports' } }
it 'does not send polling interval' do
expect(::Gitlab::PollingInterval).not_to receive(:set_header)
subject
end
it 'returns 400 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'status_reason' => 'Failed to parse api fuzzing reports' })
end
end
it_behaves_like 'authorize read pipeline'
end
describe 'GET #secret_detection_reports' do
let(:merge_request) { create(:ee_merge_request, :with_secret_detection_reports, source_project: project, author: create(:user)) }
let(:params) do
......
......@@ -157,6 +157,18 @@ FactoryBot.define do
end
end
trait :with_api_fuzzing_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
:ee_ci_pipeline,
:success,
:with_api_fuzzing_report,
project: merge_request.source_project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
end
trait :with_dast_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
......
......@@ -107,6 +107,18 @@ RSpec.describe 'Billing plan pages', :feature do
end
end
shared_examples 'used seats rendering for non paid subscriptions' do
before do
visit page_path
end
it 'displays the number of seats', :js do
page.within('.js-subscription-table') do
expect(page).to have_selector('p.property-value.gl-mt-2.gl-mb-0.number', text: '1')
end
end
end
context 'users profile billing page' do
let(:page_path) { profile_billings_path }
......@@ -343,6 +355,13 @@ RSpec.describe 'Billing plan pages', :feature do
it_behaves_like 'can contact sales'
end
context 'on free' do
let(:plan) { free_plan }
let!(:subscription) { create(:gitlab_subscription, namespace: namespace, hosted_plan: plan) }
it_behaves_like 'used seats rendering for non paid subscriptions'
end
end
end
......@@ -386,8 +405,8 @@ RSpec.describe 'Billing plan pages', :feature do
expect(page).to have_selector('.js-subscription-table')
end
it_behaves_like 'downgradable plan'
it_behaves_like 'non-upgradable plan'
it_behaves_like 'used seats rendering for non paid subscriptions'
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe SearchController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
render_views
let_it_be(:user) { create(:admin) }
before(:all) do
clean_frontend_fixtures('ee/search/')
end
before do
sign_in(user)
end
context 'search within a project', :sidekiq_inline do
let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :public, :repository, namespace: namespace, path: 'search-project') }
let(:blobs) do
Kaminari.paginate_array([
Gitlab::Search::FoundBlob.new(
path: 'CHANGELOG',
basename: 'CHANGELOG',
ref: 'master',
data: "hello\nworld\nfoo\nbar # this is the highlight\nbaz\nboo\nbat",
project: project,
project_id: project.id,
startline: 2,
highlight_line: 4),
Gitlab::Search::FoundBlob.new(
path: 'CONTRIBUTING',
basename: 'CONTRIBUTING',
ref: 'master',
data: "hello\nworld\nfoo\nbar # this is the highlight\nbaz\nboo\nbat",
project: project,
project_id: project.id,
startline: 2,
highlight_line: 4),
Gitlab::Search::FoundBlob.new(
path: 'README',
basename: 'README',
ref: 'master',
data: "foo\nbar # this is the highlight\nbaz\nboo\nbat",
project: project,
project_id: project.id,
startline: 2,
highlight_line: 2)
],
total_count: 3,
limit: 3,
offset: 0)
end
it 'ee/search/blob_search_result.html' do
expect_next_instance_of(SearchService) do |search_service|
expect(search_service).to receive(:search_objects).and_return(blobs)
end
get :show, params: {
search: 'Send',
project_id: project.id,
scope: :blobs
}
expect(response).to be_successful
end
end
end
import setHighlightClass from 'ee/search/highlight_blob_search_result';
const fixture = 'ee/search/blob_search_result.html';
const ceFixture = 'search/blob_search_result.html';
describe('ee/search/highlight_blob_search_result', () => {
preloadFixtures(fixture, ceFixture);
// Basic search support
it('highlights lines with search term occurrence', () => {
loadFixtures(ceFixture);
setHighlightClass();
expect(document.querySelectorAll('.blob-result .hll').length).toBe(4);
});
// Advanced search support
it('highlights lines which have been identified by Elasticsearch', () => {
loadFixtures(fixture);
setHighlightClass();
expect(document.querySelectorAll('.blob-result .hll').length).toBe(3);
});
});
......@@ -102,6 +102,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
expect(parsed).to be_kind_of(::Gitlab::Search::FoundBlob)
expect(parsed).to have_attributes(
startline: 1,
highlight_line: nil,
project: project,
data: "foo\n"
)
......@@ -124,6 +125,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
basename: 'path/file',
ref: 'sha',
startline: 2,
highlight_line: 2,
project: project,
data: "bar\n"
)
......@@ -177,6 +179,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
basename: 'path/file',
ref: 'sha',
startline: 2,
highlight_line: 4,
project: project,
data: expected_data
)
......
......@@ -115,6 +115,22 @@ RSpec.describe Ci::JobArtifact do
end
end
describe '.api_fuzzing_reports' do
subject { Ci::JobArtifact.api_fuzzing }
context 'when there is a metrics report' do
let!(:artifact) { create(:ee_ci_job_artifact, :api_fuzzing) }
it { is_expected.to eq([artifact]) }
end
context 'when there is no coverage fuzzing reports' do
let!(:artifact) { create(:ee_ci_job_artifact, :trace) }
it { is_expected.to be_empty }
end
end
describe '.associated_file_types_for' do
using RSpec::Parameterized::TableSyntax
......
......@@ -200,6 +200,79 @@ RSpec.describe GitlabSubscription do
end
end
describe '#seats_in_use' do
let(:group) { create(:group) }
let!(:group_member) { create(:group_member, :developer, user: create(:user), group: group) }
let(:hosted_plan) { nil }
let(:seats_in_use) { 5 }
let(:trial) { false }
let(:gitlab_subscription) do
create(:gitlab_subscription, namespace: group, trial: trial, hosted_plan: hosted_plan, seats_in_use: seats_in_use)
end
shared_examples 'a disabled feature' do
context 'when feature flag is disabled' do
before do
stub_feature_flags(seats_in_use_for_free_or_trial: false)
end
it 'returns the previously calculated seats in use' do
expect(subject).to eq(5)
end
end
end
subject { gitlab_subscription.seats_in_use }
context 'with a paid hosted plan' do
let(:hosted_plan) { gold_plan }
it 'returns the previously calculated seats in use' do
expect(subject).to eq(5)
end
context 'when seats in use is 0' do
let(:seats_in_use) { 0 }
it 'returns 0 too' do
expect(subject).to eq(0)
end
end
end
context 'with a trial plan' do
let(:hosted_plan) { gold_plan }
let(:trial) { true }
it 'returns the current seats in use' do
expect(subject).to eq(1)
end
it_behaves_like 'a disabled feature'
end
context 'with a free plan' do
let(:hosted_plan) { free_plan }
it 'returns the current seats in use' do
expect(subject).to eq(1)
end
it_behaves_like 'a disabled feature'
end
context 'with a self hosted plan' do
before do
gitlab_subscription.update!(namespace: nil)
end
it 'returns the previously calculated seats in use' do
expect(subject).to eq(5)
end
end
end
describe '#expired?' do
let(:gitlab_subscription) { create(:gitlab_subscription, end_date: end_date) }
......
......@@ -175,6 +175,7 @@ RSpec.describe MergeRequest do
:license_scanning | :with_license_management_reports | :license_scanning
:license_scanning | :with_license_scanning_reports | :license_scanning
:coverage_fuzzing | :with_coverage_fuzzing_reports | :coverage_fuzzing
:api_fuzzing | :with_api_fuzzing_reports | :api_fuzzing
end
with_them do
......@@ -403,6 +404,28 @@ RSpec.describe MergeRequest do
end
end
describe '#has_api_fuzzing_reports?' do
subject { merge_request.has_api_fuzzing_reports? }
let_it_be(:project) { create(:project, :repository) }
before do
stub_licensed_features(api_fuzzing: true)
end
context 'when head pipeline has coverage fuzzing reports' do
let(:merge_request) { create(:ee_merge_request, :with_api_fuzzing_reports, source_project: project) }
it { is_expected.to be_truthy }
end
context 'when head pipeline does not have coverage fuzzing reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#calculate_reactive_cache with current_user' do
let(:project) { create(:project, :repository) }
let(:current_user) { project.users.take }
......@@ -854,6 +877,66 @@ RSpec.describe MergeRequest do
end
end
describe '#compare_api_fuzzing_reports' do
subject { merge_request.compare_api_fuzzing_reports(current_user) }
let_it_be(:project) { create(:project, :repository) }
let(:current_user) { project.users.first }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(:ee_ci_pipeline,
:with_api_fuzzing_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
before do
merge_request.update!(head_pipeline_id: head_pipeline.id)
end
context 'when head pipeline has api fuzzing reports' do
let!(:head_pipeline) do
create(:ee_ci_pipeline,
:with_api_fuzzing_report,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
context 'when reactive cache worker is parsing asynchronously' do
it 'returns status' do
expect(subject[:status]).to eq(:parsing)
end
end
context 'when reactive cache worker is inline' do
before do
synchronous_reactive_cache(merge_request)
end
it 'returns status and data' do
expect_any_instance_of(Ci::CompareSecurityReportsService)
.to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
subject
end
context 'when cached results is not latest' do
before do
allow_any_instance_of(Ci::CompareSecurityReportsService)
.to receive(:latest?).and_return(false)
end
it 'raises an InvalidateReactiveCache error' do
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
end
end
end
end
end
describe '#mergeable_with_quick_action?' do
def create_pipeline(status)
pipeline = create(:ci_pipeline,
......
......@@ -9,7 +9,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
include BlobActiveModel
attr_reader :project, :content_match, :blob_path
attr_reader :project, :content_match, :blob_path, :highlight_line
PATH_REGEXP = /\A(?<ref>[^:]*):(?<path>[^\x00]*)\x00/.freeze
CONTENT_REGEXP = /^(?<ref>[^:]*):(?<path>[^\x00]*)\x00(?<startline>\d+)\x00/.freeze
......@@ -26,6 +26,7 @@ module Gitlab
@binary_basename = opts.fetch(:basename, nil)
@ref = opts.fetch(:ref, nil)
@startline = opts.fetch(:startline, nil)
@highlight_line = opts.fetch(:highlight_line, nil)
@binary_data = opts.fetch(:data, nil)
@per_page = opts.fetch(:per_page, 20)
@project = opts.fetch(:project, nil)
......
......@@ -24,8 +24,9 @@ Disallow: /help
Disallow: /s/
Disallow: /-/profile
Disallow: /-/ide/
# Only specifically allow the Sign In page to avoid very ugly search results
# Restrict allowed routes to avoid very ugly search results
Allow: /users/sign_in
Allow: /users/*/snippets
# Generic resource routes like new, edit, raw
# This will block routes like:
......
......@@ -26,8 +26,51 @@ RSpec.describe SearchController, '(JavaScript fixtures)', type: :controller do
context 'search within a project' do
let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :public, :repository, namespace: namespace, path: 'search-project') }
let(:blobs) do
Kaminari.paginate_array([
Gitlab::Search::FoundBlob.new(
path: 'CHANGELOG',
basename: 'CHANGELOG',
ref: 'master',
data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat",
project: project,
project_id: project.id,
startline: 2),
Gitlab::Search::FoundBlob.new(
path: 'CONTRIBUTING',
basename: 'CONTRIBUTING',
ref: 'master',
data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat",
project: project,
project_id: project.id,
startline: 2),
Gitlab::Search::FoundBlob.new(
path: 'README',
basename: 'README',
ref: 'master',
data: "foo\nSend # this is the highlight\nbaz\nboo\nbat",
project: project,
project_id: project.id,
startline: 2),
Gitlab::Search::FoundBlob.new(
path: 'test',
basename: 'test',
ref: 'master',
data: "foo\nSend # this is the highlight\nbaz\nboo\nbat",
project: project,
project_id: project.id,
startline: 2)
],
total_count: 4,
limit: 4,
offset: 0)
end
it 'search/blob_search_result.html' do
expect_next_instance_of(SearchService) do |search_service|
expect(search_service).to receive(:search_objects).and_return(blobs)
end
get :show, params: {
search: 'Send',
project_id: project.id,
......
import setHighlightClass from '~/pages/search/show/highlight_blob_search_result';
import setHighlightClass from '~/search/highlight_blob_search_result';
const fixture = 'search/blob_search_result.html';
describe('pages/search/show/highlight_blob_search_result', () => {
describe('search/highlight_blob_search_result', () => {
preloadFixtures(fixture);
beforeEach(() => loadFixtures(fixture));
......@@ -10,6 +10,6 @@ describe('pages/search/show/highlight_blob_search_result', () => {
it('highlights lines with search term occurrence', () => {
setHighlightClass();
expect(document.querySelectorAll('.blob-result .hll').length).toBe(11);
expect(document.querySelectorAll('.blob-result .hll').length).toBe(4);
});
});
import $ from 'jquery';
import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import Api from '~/api';
import Search from '~/pages/search/show/search';
import setHighlightClass from '~/pages/search/show/highlight_blob_search_result';
jest.mock('~/api');
jest.mock('~/pages/search/show/highlight_blob_search_result');
jest.mock('ee_else_ce/search/highlight_blob_search_result');
describe('Search', () => {
const fixturePath = 'search/show.html';
......
......@@ -135,11 +135,31 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
context 'when data_store is fog' do
let(:data_store) { :fog }
before do
build_trace_chunk.send(:unsafe_set_data!, +'Sample data in fog')
context 'when legacy Fog is enabled' do
before do
stub_feature_flags(ci_trace_new_fog_store: false)
build_trace_chunk.send(:unsafe_set_data!, +'Sample data in fog')
end
it { is_expected.to eq('Sample data in fog') }
it 'returns a LegacyFog store' do
expect(described_class.get_store_class(data_store)).to be_a(Ci::BuildTraceChunks::LegacyFog)
end
end
it { is_expected.to eq('Sample data in fog') }
context 'when new Fog is enabled' do
before do
stub_feature_flags(ci_trace_new_fog_store: true)
build_trace_chunk.send(:unsafe_set_data!, +'Sample data in fog')
end
it { is_expected.to eq('Sample data in fog') }
it 'returns a new Fog store' do
expect(described_class.get_store_class(data_store)).to be_a(Ci::BuildTraceChunks::Fog)
end
end
end
end
......
......@@ -4,8 +4,12 @@ require 'spec_helper'
RSpec.describe Ci::BuildTraceChunks::Fog do
let(:data_store) { described_class.new }
let(:bucket) { 'artifacts' }
let(:connection_params) { Gitlab.config.artifacts.object_store.connection.symbolize_keys }
let(:connection) { ::Fog::Storage.new(connection_params) }
before do
stub_object_storage(connection_params: connection_params, remote_directory: bucket)
stub_artifacts_object_storage
end
......@@ -148,17 +152,17 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
end
it 'deletes multiple data' do
::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection|
expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body]).to be_present
expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body]).to be_present
end
files = connection.directories.new(key: bucket).files
expect(files.count).to eq(2)
expect(files[0].body).to be_present
expect(files[1].body).to be_present
subject
::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection|
expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body] }.to raise_error(Excon::Error::NotFound)
expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body] }.to raise_error(Excon::Error::NotFound)
end
files.reload
expect(files.count).to eq(0)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::BuildTraceChunks::LegacyFog do
let(:data_store) { described_class.new }
before do
stub_artifacts_object_storage
end
describe '#available?' do
subject { data_store.available? }
context 'when object storage is enabled' do
it { is_expected.to be_truthy }
end
context 'when object storage is disabled' do
before do
stub_artifacts_object_storage(enabled: false)
end
it { is_expected.to be_falsy }
end
end
describe '#data' do
subject { data_store.data(model) }
context 'when data exists' do
let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
it 'returns the data' do
is_expected.to eq('sample data in fog')
end
end
context 'when data does not exist' do
let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
it 'returns nil' do
expect(data_store.data(model)).to be_nil
end
end
end
describe '#set_data' do
let(:new_data) { 'abc123' }
context 'when data exists' do
let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
it 'overwrites data' do
expect(data_store.data(model)).to eq('sample data in fog')
data_store.set_data(model, new_data)
expect(data_store.data(model)).to eq new_data
end
end
context 'when data does not exist' do
let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
it 'sets new data' do
expect(data_store.data(model)).to be_nil
data_store.set_data(model, new_data)
expect(data_store.data(model)).to eq new_data
end
end
end
describe '#delete_data' do
subject { data_store.delete_data(model) }
context 'when data exists' do
let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
it 'deletes data' do
expect(data_store.data(model)).to eq('sample data in fog')
subject
expect(data_store.data(model)).to be_nil
end
end
context 'when data does not exist' do
let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
it 'does nothing' do
expect(data_store.data(model)).to be_nil
subject
expect(data_store.data(model)).to be_nil
end
end
end
describe '#size' do
context 'when data exists' do
let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'üabcd') }
it 'returns data bytesize correctly' do
expect(data_store.size(model)).to eq 6
end
end
context 'when data does not exist' do
let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
it 'returns zero' do
expect(data_store.size(model)).to be_zero
end
end
end
describe '#keys' do
subject { data_store.keys(relation) }
let(:build) { create(:ci_build) }
let(:relation) { build.trace_chunks }
before do
create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 0, build: build)
create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 1, build: build)
end
it 'returns keys' do
is_expected.to eq([[build.id, 0], [build.id, 1]])
end
end
describe '#delete_keys' do
subject { data_store.delete_keys(keys) }
let(:build) { create(:ci_build) }
let(:relation) { build.trace_chunks }
let(:keys) { data_store.keys(relation) }
before do
create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 0, build: build)
create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 1, build: build)
end
it 'deletes multiple data' do
::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection|
expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body]).to be_present
expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body]).to be_present
end
subject
::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection|
expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body] }.to raise_error(Excon::Error::NotFound)
expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body] }.to raise_error(Excon::Error::NotFound)
end
end
end
end
......@@ -843,5 +843,24 @@ RSpec.describe Service do
service.log_error(test_message, additional_argument: 'some argument')
end
context 'when project is nil' do
let(:project) { nil }
let(:arguments) do
{
service_class: service.class.name,
project_path: nil,
project_id: nil,
message: test_message,
additional_argument: 'some argument'
}
end
it 'logs info messages using json logger' do
expect(Gitlab::JsonLogger).to receive(:info).with(arguments)
service.log_info(test_message, additional_argument: 'some argument')
end
end
end
end
......@@ -14,7 +14,9 @@ RSpec.describe 'Robots.txt Requests', :aggregate_failures do
it 'allows the requests' do
requests = [
'/users/sign_in',
'/namespace/subnamespace/design.gitlab.com'
'/namespace/subnamespace/design.gitlab.com',
'/users/foo/snippets',
'/users/foo/snippets/1'
]
requests.each do |request|
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment