Description: manual merge caused a mismatch this patch fixes the mismatch Author: Pirate Praveen --- Last-Update: 2021-01-30 --- /dev/null +++ gitlab-13.6.5/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue @@ -0,0 +1,215 @@ + + --- /dev/null +++ gitlab-13.6.5/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql @@ -0,0 +1,76 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" +#import "./count.fragment.graphql" + +query pipelineStats( + $firstTotal: Int + $firstSucceeded: Int + $firstFailed: Int + $firstCanceled: Int + $firstSkipped: Int + $endCursorTotal: String + $endCursorSucceeded: String + $endCursorFailed: String + $endCursorCanceled: String + $endCursorSkipped: String +) { + pipelinesTotal: instanceStatisticsMeasurements( + identifier: PIPELINES + first: $firstTotal + after: $endCursorTotal + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } + pipelinesSucceeded: instanceStatisticsMeasurements( + identifier: PIPELINES_SUCCEEDED + first: $firstSucceeded + after: $endCursorSucceeded + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } + pipelinesFailed: instanceStatisticsMeasurements( + identifier: PIPELINES_FAILED + first: $firstFailed + after: $endCursorFailed + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } + pipelinesCanceled: instanceStatisticsMeasurements( + identifier: PIPELINES_CANCELED + first: $firstCanceled + after: $endCursorCanceled + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } + pipelinesSkipped: instanceStatisticsMeasurements( + identifier: PIPELINES_SKIPPED + first: $firstSkipped + after: $endCursorSkipped + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } +} --- /dev/null +++ gitlab-13.6.5/app/assets/javascripts/performance_utils.js @@ -0,0 +1,10 @@ +export const performanceMarkAndMeasure = ({ mark, measures = [] } = {}) => { + window.requestAnimationFrame(() => { + if (mark && !performance.getEntriesByName(mark).length) { + performance.mark(mark); + } + measures.forEach(measure => { + performance.measure(measure.name, measure.start, measure.end); + }); + }); +}; --- /dev/null +++ gitlab-13.6.5/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue @@ -0,0 +1,100 @@ + + + --- /dev/null +++ gitlab-13.6.5/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js @@ -0,0 +1,36 @@ +import { __ } from '~/locale'; + +const header = __('Confidentiality'); + +const filters = { + ANY: { + label: __('Any'), + value: null, + }, + CONFIDENTIAL: { + label: __('Confidential'), + value: 'yes', + }, + NOT_CONFIDENTIAL: { + label: __('Not confidential'), + value: 'no', + }, +}; + +const scopes = { + ISSUES: 'issues', +}; + +const filterByScope = { + [scopes.ISSUES]: [filters.ANY, filters.CONFIDENTIAL, filters.NOT_CONFIDENTIAL], +}; + +const filterParam = 'confidential'; + +export default { + header, + filters, + scopes, + filterByScope, + filterParam, +}; --- /dev/null +++ gitlab-13.6.5/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js @@ -0,0 +1,42 @@ +import { __ } from '~/locale'; + +const header = __('Status'); + +const filters = { + ANY: { + label: __('Any'), + value: 'all', + }, + OPEN: { + label: __('Open'), + value: 'opened', + }, + CLOSED: { + label: __('Closed'), + value: 'closed', + }, + MERGED: { + label: __('Merged'), + value: 'merged', + }, +}; + +const scopes = { + ISSUES: 'issues', + MERGE_REQUESTS: 'merge_requests', +}; + +const filterByScope = { + [scopes.ISSUES]: [filters.ANY, filters.OPEN, filters.CLOSED], + [scopes.MERGE_REQUESTS]: [filters.ANY, filters.OPEN, filters.MERGED, filters.CLOSED], +}; + +const filterParam = 'state'; + +export default { + header, + filters, + scopes, + filterByScope, + filterParam, +}; --- /dev/null +++ gitlab-13.6.5/app/assets/javascripts/search/dropdown_filter/index.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import DropdownFilter from './components/dropdown_filter.vue'; +import stateFilterData from './constants/state_filter_data'; +import confidentialFilterData from './constants/confidential_filter_data'; + +Vue.use(Translate); + +const mountDropdownFilter = (store, { id, filterData }) => { + const el = document.getElementById(id); + + if (!el) return false; + + return new Vue({ + el, + store, + render(createElement) { + return createElement(DropdownFilter, { + props: { + filterData, + }, + }); + }, + }); +}; + +const dropdownFilters = [ + { + id: 'js-search-filter-by-state', + filterData: stateFilterData, + }, + { + id: 'js-search-filter-by-confidential', + filterData: confidentialFilterData, + }, +]; + +export default store => [...dropdownFilters].map(filter => mountDropdownFilter(store, filter)); --- /dev/null +++ gitlab-13.6.5/app/assets/stylesheets/page_bundles/experimental_separate_sign_up.scss @@ -0,0 +1,96 @@ +@import 'mixins_and_variables_and_functions'; + +.signup-page { + .page-wrap { + background-color: var(--gray-10, $gray-10); + } + + .signup-box-container { + max-width: 960px; + } + + .signup-box { + background-color: var(--white, $white); + box-shadow: 0 0 0 1px var(--border-color, $border-color); + border-radius: $border-radius; + } + + .form-control { + &:active, + &:focus { + background-color: var(--white, $white); + } + } + + .devise-errors { + h2 { + font-size: $gl-font-size; + color: var(--red-700, $red-700); + } + } + + .omniauth-divider { + &::before, + &::after { + content: ''; + flex: 1; + border-bottom: 1px solid var(--gray-100, $gray-100); + margin: $gl-padding-24 0; + } + + &::before { + margin-right: $gl-padding; + } + + &::after { + margin-left: $gl-padding; + } + } + + .omniauth-btn { + width: 48%; + + @include media-breakpoint-down(md) { + width: 100%; + } + + img { + width: $default-icon-size; + height: $default-icon-size; + } + } + + .decline-page { + width: 350px; + } +} + +.signup-page[data-page^='registrations:experience_levels'] { + $card-shadow-color: rgba(var(--black, $black), 0.2); + + .page-wrap { + background-color: var(--white, $white); + } + + .card-deck { + max-width: 828px; + } + + .card { + transition: box-shadow 0.3s ease-in-out; + } + + .card:hover { + box-shadow: 0 $gl-spacing-scale-3 $gl-spacing-scale-5 $card-shadow-color; + } + + @media (min-width: $breakpoint-sm) { + .card-deck .card { + margin: 0 $gl-spacing-scale-3; + } + } + + .stretched-link:hover { + text-decoration: none; + } +} --- /dev/null +++ gitlab-13.6.5/app/models/ci/build_trace_chunks/legacy_fog.rb @@ -0,0 +1,77 @@ +# 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 --- /dev/null +++ gitlab-13.6.5/app/views/search/results/_filters.html.haml @@ -0,0 +1,7 @@ +.d-lg-flex.align-items-end + #js-search-filter-by-state{ 'v-cloak': true } + - if Feature.enabled?(:search_filter_by_confidential, @group) + #js-search-filter-by-confidential{ 'v-cloak': true } + + - if %w(issues merge_requests).include?(@scope) + %hr.gl-mt-4.gl-mb-4 --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/admin_approval_for_new_user_signups.yml @@ -0,0 +1,7 @@ +--- +name: admin_approval_for_new_user_signups +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43827 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258980 +type: development +group: group::access +default_enabled: true --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/ci_always_refresh_merge_requests_from_beginning.yml @@ -0,0 +1,7 @@ +--- +name: ci_always_refresh_merge_requests_from_beginning +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45232 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/268215 +type: development +group: group::continuous integration +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/ci_delete_objects_low_concurrency.yml @@ -0,0 +1,7 @@ +--- +name: ci_delete_objects_low_concurrency +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39464 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247103 +group: group::continuous integration +type: development +default_enabled: false \ No newline at end of file --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/ci_send_deployment_hook_when_start.yml @@ -0,0 +1,7 @@ +--- +name: ci_send_deployment_hook_when_start +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41214 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247137 +group: group::progressive delivery +type: development +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/ci_trace_new_fog_store.yml @@ -0,0 +1,7 @@ +--- +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: true --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/deploy_boards_dedupe_instances.yml @@ -0,0 +1,7 @@ +--- +name: deploy_boards_dedupe_instances +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40768 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258214 +type: development +group: group::progressive delivery +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/deployment_filters.yml @@ -0,0 +1,7 @@ +--- +name: deployment_filters +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44041 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267561 +type: development +group: group::source code +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/disable_shared_runners_on_group.yml @@ -0,0 +1,7 @@ +--- +name: disable_shared_runners_on_group +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36080 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258991 +type: development +group: group::runner +default_enabled: true --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/expose_environment_path_in_alert_details.yml @@ -0,0 +1,7 @@ +--- +name: expose_environment_path_in_alert_details +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43414 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258638 +type: development +group: group::progressive delivery +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/kubernetes_cluster_namespace_role_admin.yml @@ -0,0 +1,7 @@ +--- +name: kubernetes_cluster_namespace_role_admin +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45479 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/270030 +type: development +group: group::configure +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/merge_base_pipelines.yml @@ -0,0 +1,7 @@ +--- +name: merge_base_pipelines +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44648 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263724 +type: development +group: group::testing +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/one_dimensional_matrix.yml @@ -0,0 +1,7 @@ +--- +name: one_dimensional_matrix +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42170 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/256062 +type: development +group: group::pipeline authoring +default_enabled: true --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/search_filter_by_confidential.yml @@ -0,0 +1,7 @@ +--- +name: search_filter_by_confidential +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40793 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/244923 +group: group::global search +type: development +default_enabled: false \ No newline at end of file --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/soft_fail_count_by_state.yml @@ -0,0 +1,7 @@ +--- +name: soft_fail_count_by_state +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44184 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263222 +type: development +group: group::source code +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/sync_metrics_dashboards.yml @@ -0,0 +1,7 @@ +--- +name: sync_metrics_dashboards +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39658 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241793 +group: group::apm +type: development +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/development/track_unique_test_cases_parsed.yml @@ -0,0 +1,7 @@ +--- +name: track_unique_test_cases_parsed +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41918 +rollout_issue_url: +group: group::testing +type: development +default_enabled: false --- /dev/null +++ gitlab-13.6.5/config/feature_flags/licensed/incident_sla.yml @@ -0,0 +1,7 @@ +--- +name: incident_sla +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43648 +rollout_issue_url: +group: group::health +type: licensed +default_enabled: true --- /dev/null +++ gitlab-13.6.5/config/feature_flags/licensed/minimal_access_role.yml @@ -0,0 +1,7 @@ +--- +name: minimal_access_role +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40942 +rollout_issue_url: +group: group::access +type: licensed +default_enabled: true --- /dev/null +++ gitlab-13.6.5/config/feature_flags/licensed/resource_access_token.yml @@ -0,0 +1,7 @@ +--- +name: resource_access_token +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29622 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235765 +group: group::access +type: licensed +default_enabled: true --- /dev/null +++ gitlab-13.6.5/doc/user/admin_area/analytics/instance_statistics.md @@ -0,0 +1,18 @@ +# Instance Statistics + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235754) in GitLab 13.4. + +Instance Statistics gives you an overview of how much data your instance contains, and how quickly this volume is changing over time. + +## Total counts + +At the top of the page, Instance Statistics shows total counts for: + +- Users +- Projects +- Groups +- Issues +- Merge Requests +- Pipelines + +These figures can be useful for understanding how much data your instance contains in total. --- /dev/null +++ gitlab-13.6.5/lib/gitlab/bulk_import/client.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Gitlab + module BulkImport + class Client + API_VERSION = 'v4'.freeze + DEFAULT_PAGE = 1.freeze + DEFAULT_PER_PAGE = 30.freeze + + ConnectionError = Class.new(StandardError) + + def initialize(uri:, token:, page: DEFAULT_PAGE, per_page: DEFAULT_PER_PAGE, api_version: API_VERSION) + @uri = URI.parse(uri) + @token = token&.strip + @page = page + @per_page = per_page + @api_version = api_version + end + + def get(resource, query = {}) + response = with_error_handling do + Gitlab::HTTP.get( + resource_url(resource), + headers: request_headers, + follow_redirects: false, + query: query.merge(request_query) + ) + end + + response.parsed_response + end + + private + + def request_query + { + page: @page, + per_page: @per_page + } + end + + def request_headers + { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{@token}" + } + end + + def with_error_handling + response = yield + + raise ConnectionError.new("Error #{response.code}") unless response.success? + + response + rescue *Gitlab::HTTP::HTTP_ERRORS => e + raise ConnectionError, e + end + + def base_uri + @base_uri ||= "#{@uri.scheme}://#{@uri.host}:#{@uri.port}" + end + + def api_url + Gitlab::Utils.append_path(base_uri, "/api/#{@api_version}") + end + + def resource_url(resource) + Gitlab::Utils.append_path(api_url, resource) + end + end + end +end --- /dev/null +++ gitlab-13.6.5/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + module Project + module Sample + class SampleDataRelationTreeRestorer < RelationTreeRestorer + DATE_MODELS = %i[issues milestones].freeze + + def initialize(*args) + super + + date_calculator + end + + private + + def build_relation(relation_key, relation_definition, data_hash) + # Override due date attributes in data hash for Sample Data templates + # Dates are moved by taking the closest one to average and moving that (and rest around it) to the date of import + # TODO: To move this logic to RelationFactory (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465333) + override_date_attributes!(relation_key, data_hash) + super + end + + def override_date_attributes!(relation_key, data_hash) + return unless DATE_MODELS.include?(relation_key.to_sym) + + data_hash['start_date'] = date_calculator.calculate_by_closest_date_to_average(data_hash['start_date'].to_time) unless data_hash['start_date'].nil? + data_hash['due_date'] = date_calculator.calculate_by_closest_date_to_average(data_hash['due_date'].to_time) unless data_hash['due_date'].nil? + end + + # TODO: Move clear logic into main comsume_relation method (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465330) + def dates + unless relation_reader.legacy? + DATE_MODELS.map do |tag| + relation_reader.consume_relation(@importable_path, tag).map { |model| model.first['due_date'] }.tap do + relation_reader.clear_consumed_relations + end + end + end + end + + def date_calculator + @date_calculator ||= Gitlab::ImportExport::Project::Sample::DateCalculator.new(dates) + end + end + end + end + end +end --- /dev/null +++ gitlab-13.6.5/lib/gitlab/middleware/handle_null_bytes.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Gitlab + module Middleware + # There is no valid reason for a request to contain a null byte (U+0000) + # so just return HTTP 400 (Bad Request) if we receive one + class HandleNullBytes + NULL_BYTE_REGEX = Regexp.new(Regexp.escape("\u0000")).freeze + + attr_reader :app + + def initialize(app) + @app = app + end + + def call(env) + return [400, {}, ["Bad Request"]] if request_has_null_byte?(env) + + app.call(env) + end + + private + + def request_has_null_byte?(request) + return false if ENV['REJECT_NULL_BYTES'] == "1" + + request = Rack::Request.new(request) + + request.params.values.any? do |value| + param_has_null_byte?(value) + end + end + + def param_has_null_byte?(value, depth = 0) + # Guard against possible attack sending large amounts of nested params + # Should be safe as deeply nested params are highly uncommon. + return false if depth > 2 + + depth += 1 + + if value.respond_to?(:match) + string_contains_null_byte?(value) + elsif value.respond_to?(:values) + value.values.any? do |hash_value| + param_has_null_byte?(hash_value, depth) + end + elsif value.is_a?(Array) + value.any? do |array_value| + param_has_null_byte?(array_value, depth) + end + else + false + end + end + + def string_contains_null_byte?(string) + string.match?(NULL_BYTE_REGEX) + end + end + end +end --- /dev/null +++ gitlab-13.6.5/spec/fixtures/packages/debian/libsample0_1.2.3~alpha2-1_amd64.deb @@ -0,0 +1 @@ +empty --- /dev/null +++ gitlab-13.6.5/spec/frontend/alert_settings/alerts_integrations_list_spec.js @@ -0,0 +1,89 @@ +import { GlTable, GlIcon } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Tracking from '~/tracking'; +import AlertIntegrationsList, { + i18n, +} from '~/alerts_settings/components/alerts_integrations_list.vue'; +import { trackAlertIntergrationsViewsOptions } from '~/alerts_settings/constants'; + +const mockIntegrations = [ + { + activated: true, + name: 'Integration 1', + type: 'HTTP endpoint', + }, + { + activated: false, + name: 'Integration 2', + type: 'HTTP endpoint', + }, +]; + +describe('AlertIntegrationsList', () => { + let wrapper; + + function mountComponent(propsData = {}) { + wrapper = mount(AlertIntegrationsList, { + propsData: { + integrations: mockIntegrations, + ...propsData, + }, + stubs: { + GlIcon: true, + }, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + beforeEach(() => { + mountComponent(); + }); + + const findTableComponent = () => wrapper.find(GlTable); + const finsStatusCell = () => wrapper.findAll('[data-testid="integration-activated-status"]'); + + it('renders a table', () => { + expect(findTableComponent().exists()).toBe(true); + }); + + it('renders an empty state when no integrations provided', () => { + mountComponent({ integrations: [] }); + expect(findTableComponent().text()).toContain(i18n.emptyState); + }); + + describe('integration status', () => { + it('enabled', () => { + const cell = finsStatusCell().at(0); + const activatedIcon = cell.find(GlIcon); + expect(cell.text()).toBe(i18n.status.enabled.name); + expect(activatedIcon.attributes('name')).toBe('check-circle-filled'); + expect(activatedIcon.attributes('title')).toBe(i18n.status.enabled.tooltip); + }); + + it('disabled', () => { + const cell = finsStatusCell().at(1); + const notActivatedIcon = cell.find(GlIcon); + expect(cell.text()).toBe(i18n.status.disabled.name); + expect(notActivatedIcon.attributes('name')).toBe('warning-solid'); + expect(notActivatedIcon.attributes('title')).toBe(i18n.status.disabled.tooltip); + }); + }); + + describe('Snowplow tracking', () => { + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + mountComponent(); + }); + + it('should track alert list page views', () => { + const { category, action } = trackAlertIntergrationsViewsOptions; + expect(Tracking.event).toHaveBeenCalledWith(category, action); + }); + }); +}); --- /dev/null +++ gitlab-13.6.5/spec/frontend/analytics/instance_statistics/components/__snapshots__/pipelines_chart_spec.js.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinesChart when fetching more data when the fetchMore query returns data passes the data to the line chart 1`] = ` +Array [ + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Total", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Succeeded", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 22, + ], + Array [ + "2020-07-01", + 41, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Failed", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Canceled", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Skipped", + }, +] +`; + +exports[`PipelinesChart with data passes the data to the line chart 1`] = ` +Array [ + Object { + "data": Array [ + Array [ + "2020-06-01", + 22, + ], + Array [ + "2020-07-01", + 41, + ], + ], + "name": "Total", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + ], + "name": "Succeeded", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + ], + "name": "Failed", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 22, + ], + Array [ + "2020-07-01", + 41, + ], + ], + "name": "Canceled", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 22, + ], + Array [ + "2020-07-01", + 41, + ], + ], + "name": "Skipped", + }, +] +`; --- /dev/null +++ gitlab-13.6.5/spec/frontend/analytics/instance_statistics/components/pipelines_chart_spec.js @@ -0,0 +1,189 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlLineChart } from '@gitlab/ui/dist/charts'; +import { GlAlert } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import PipelinesChart from '~/analytics/instance_statistics/components/pipelines_chart.vue'; +import pipelinesStatsQuery from '~/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql'; +import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; +import { mockCountsData1, mockCountsData2 } from '../mock_data'; +import { getApolloResponse } from '../apollo_mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('PipelinesChart', () => { + let wrapper; + let queryHandler; + + const createApolloProvider = pipelineStatsHandler => { + return createMockApollo([[pipelinesStatsQuery, pipelineStatsHandler]]); + }; + + const createComponent = apolloProvider => { + return shallowMount(PipelinesChart, { + localVue, + apolloProvider, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findLoader = () => wrapper.find(ChartSkeletonLoader); + const findChart = () => wrapper.find(GlLineChart); + const findAlert = () => wrapper.find(GlAlert); + + describe('while loading', () => { + beforeEach(() => { + queryHandler = jest.fn().mockReturnValue(new Promise(() => {})); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + }); + + it('requests data', () => { + expect(queryHandler).toBeCalledTimes(1); + }); + + it('displays the skeleton loader', () => { + expect(findLoader().exists()).toBe(true); + }); + + it('hides the chart', () => { + expect(findChart().exists()).toBe(false); + }); + + it('does not show an error', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('without data', () => { + beforeEach(() => { + const emptyResponse = getApolloResponse(); + queryHandler = jest.fn().mockResolvedValue(emptyResponse); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + }); + + it('renders an no data message', () => { + expect(findAlert().text()).toBe('There is no data available.'); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(false); + }); + }); + + describe('with data', () => { + beforeEach(() => { + const response = getApolloResponse({ + pipelinesTotal: mockCountsData1, + pipelinesSucceeded: mockCountsData2, + pipelinesFailed: mockCountsData2, + pipelinesCanceled: mockCountsData1, + pipelinesSkipped: mockCountsData1, + }); + queryHandler = jest.fn().mockResolvedValue(response); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + }); + + it('requests data', () => { + expect(queryHandler).toBeCalledTimes(1); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(true); + }); + + it('passes the data to the line chart', () => { + expect(findChart().props('data')).toMatchSnapshot(); + }); + + it('does not show an error', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('when fetching more data', () => { + const recordedAt = '2020-08-01'; + describe('when the fetchMore query returns data', () => { + beforeEach(async () => { + const newData = { recordedAt, count: 5 }; + const firstResponse = getApolloResponse({ + pipelinesTotal: mockCountsData2, + pipelinesSucceeded: mockCountsData2, + pipelinesFailed: mockCountsData1, + pipelinesCanceled: mockCountsData2, + pipelinesSkipped: mockCountsData2, + hasNextPage: true, + }); + const secondResponse = getApolloResponse({ + pipelinesTotal: [newData], + pipelinesSucceeded: [newData], + pipelinesFailed: [newData], + pipelinesCanceled: [newData], + pipelinesSkipped: [newData], + hasNextPage: false, + }); + queryHandler = jest + .fn() + .mockResolvedValueOnce(firstResponse) + .mockResolvedValueOnce(secondResponse); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + + await wrapper.vm.$nextTick(); + }); + + it('requests data twice', () => { + expect(queryHandler).toBeCalledTimes(2); + }); + + it('passes the data to the line chart', () => { + expect(findChart().props('data')).toMatchSnapshot(); + }); + }); + + describe('when the fetchMore query throws an error', () => { + beforeEach(async () => { + const response = getApolloResponse({ + pipelinesTotal: mockCountsData2, + pipelinesSucceeded: mockCountsData2, + pipelinesFailed: mockCountsData1, + pipelinesCanceled: mockCountsData2, + pipelinesSkipped: mockCountsData2, + hasNextPage: true, + }); + queryHandler = jest.fn().mockResolvedValue(response); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + jest + .spyOn(wrapper.vm.$apollo.queries.pipelineStats, 'fetchMore') + .mockImplementation(jest.fn().mockRejectedValue()); + await wrapper.vm.$nextTick(); + }); + + it('calls fetchMore', () => { + expect(wrapper.vm.$apollo.queries.pipelineStats.fetchMore).toHaveBeenCalledTimes(1); + }); + + it('show an error message', () => { + expect(findAlert().text()).toBe( + 'Could not load the pipelines chart. Please refresh the page to try again.', + ); + }); + }); + }); +}); --- /dev/null +++ gitlab-13.6.5/spec/frontend/search/dropdown_filter/components/dropdown_filter_spec.js @@ -0,0 +1,196 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import * as urlUtils from '~/lib/utils/url_utility'; +import initStore from '~/search/store'; +import DropdownFilter from '~/search/dropdown_filter/components/dropdown_filter.vue'; +import stateFilterData from '~/search/dropdown_filter/constants/state_filter_data'; +import confidentialFilterData from '~/search/dropdown_filter/constants/confidential_filter_data'; +import { MOCK_QUERY } from '../mock_data'; + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), + setUrlParams: jest.fn(), +})); + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('DropdownFilter', () => { + let wrapper; + let store; + + const createStore = options => { + store = initStore({ query: MOCK_QUERY, ...options }); + }; + + const createComponent = (props = { filterData: stateFilterData }) => { + wrapper = shallowMount(DropdownFilter, { + localVue, + store, + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + store = null; + }); + + const findGlDropdown = () => wrapper.find(GlDropdown); + const findGlDropdownItems = () => findGlDropdown().findAll(GlDropdownItem); + const findDropdownItemsText = () => findGlDropdownItems().wrappers.map(w => w.text()); + const firstDropDownItem = () => findGlDropdownItems().at(0); + + describe('StatusFilter', () => { + describe('template', () => { + describe.each` + scope | showDropdown + ${'issues'} | ${true} + ${'merge_requests'} | ${true} + ${'projects'} | ${false} + ${'milestones'} | ${false} + ${'users'} | ${false} + ${'notes'} | ${false} + ${'wiki_blobs'} | ${false} + ${'blobs'} | ${false} + `(`dropdown`, ({ scope, showDropdown }) => { + beforeEach(() => { + createStore({ query: { ...MOCK_QUERY, scope } }); + createComponent(); + }); + + it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => { + expect(findGlDropdown().exists()).toBe(showDropdown); + }); + }); + + describe.each` + initialFilter | label + ${stateFilterData.filters.ANY.value} | ${`Any ${stateFilterData.header}`} + ${stateFilterData.filters.OPEN.value} | ${stateFilterData.filters.OPEN.label} + ${stateFilterData.filters.CLOSED.value} | ${stateFilterData.filters.CLOSED.label} + `(`filter text`, ({ initialFilter, label }) => { + describe(`when initialFilter is ${initialFilter}`, () => { + beforeEach(() => { + createStore({ query: { ...MOCK_QUERY, [stateFilterData.filterParam]: initialFilter } }); + createComponent(); + }); + + it(`sets dropdown label to ${label}`, () => { + expect(findGlDropdown().attributes('text')).toBe(label); + }); + }); + }); + }); + + describe('Filter options', () => { + beforeEach(() => { + createStore(); + createComponent(); + }); + + it('renders a dropdown item for each filterOption', () => { + expect(findDropdownItemsText()).toStrictEqual( + stateFilterData.filterByScope[stateFilterData.scopes.ISSUES].map(v => { + return v.label; + }), + ); + }); + + it('clicking a dropdown item calls setUrlParams', () => { + const filter = stateFilterData.filters[Object.keys(stateFilterData.filters)[0]].value; + firstDropDownItem().vm.$emit('click'); + + expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ + [stateFilterData.filterParam]: filter, + }); + }); + + it('clicking a dropdown item calls visitUrl', () => { + firstDropDownItem().vm.$emit('click'); + + expect(urlUtils.visitUrl).toHaveBeenCalled(); + }); + }); + }); + + describe('ConfidentialFilter', () => { + describe('template', () => { + describe.each` + scope | showDropdown + ${'issues'} | ${true} + ${'merge_requests'} | ${false} + ${'projects'} | ${false} + ${'milestones'} | ${false} + ${'users'} | ${false} + ${'notes'} | ${false} + ${'wiki_blobs'} | ${false} + ${'blobs'} | ${false} + `(`dropdown`, ({ scope, showDropdown }) => { + beforeEach(() => { + createStore({ query: { ...MOCK_QUERY, scope } }); + createComponent({ filterData: confidentialFilterData }); + }); + + it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => { + expect(findGlDropdown().exists()).toBe(showDropdown); + }); + }); + + describe.each` + initialFilter | label + ${confidentialFilterData.filters.ANY.value} | ${`Any ${confidentialFilterData.header}`} + ${confidentialFilterData.filters.CONFIDENTIAL.value} | ${confidentialFilterData.filters.CONFIDENTIAL.label} + ${confidentialFilterData.filters.NOT_CONFIDENTIAL.value} | ${confidentialFilterData.filters.NOT_CONFIDENTIAL.label} + `(`filter text`, ({ initialFilter, label }) => { + describe(`when initialFilter is ${initialFilter}`, () => { + beforeEach(() => { + createStore({ + query: { ...MOCK_QUERY, [confidentialFilterData.filterParam]: initialFilter }, + }); + createComponent({ filterData: confidentialFilterData }); + }); + + it(`sets dropdown label to ${label}`, () => { + expect(findGlDropdown().attributes('text')).toBe(label); + }); + }); + }); + }); + }); + + describe('Filter options', () => { + beforeEach(() => { + createStore(); + createComponent({ filterData: confidentialFilterData }); + }); + + it('renders a dropdown item for each filterOption', () => { + expect(findDropdownItemsText()).toStrictEqual( + confidentialFilterData.filterByScope[confidentialFilterData.scopes.ISSUES].map(v => { + return v.label; + }), + ); + }); + + it('clicking a dropdown item calls setUrlParams', () => { + const filter = + confidentialFilterData.filters[Object.keys(confidentialFilterData.filters)[0]].value; + firstDropDownItem().vm.$emit('click'); + + expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ + [confidentialFilterData.filterParam]: filter, + }); + }); + + it('clicking a dropdown item calls visitUrl', () => { + firstDropDownItem().vm.$emit('click'); + + expect(urlUtils.visitUrl).toHaveBeenCalled(); + }); + }); +}); --- /dev/null +++ gitlab-13.6.5/spec/frontend/search/dropdown_filter/mock_data.js @@ -0,0 +1,5 @@ +export const MOCK_QUERY = { + scope: 'issues', + state: 'all', + confidential: null, +}; --- /dev/null +++ gitlab-13.6.5/spec/lib/gitlab/bulk_import/client_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BulkImport::Client do + include ImportSpecHelper + + let(:uri) { 'http://gitlab.example' } + let(:token) { 'token' } + let(:resource) { 'resource' } + + subject { described_class.new(uri: uri, token: token) } + + describe '#get' do + let(:response_double) { double(code: 200, success?: true, parsed_response: {}) } + + shared_examples 'performs network request' do + it 'performs network request' do + expect(Gitlab::HTTP).to receive(:get).with(*expected_args).and_return(response_double) + + subject.get(resource) + end + end + + describe 'parsed response' do + it 'returns parsed response' do + response_double = double(code: 200, success?: true, parsed_response: [{ id: 1 }, { id: 2 }]) + + allow(Gitlab::HTTP).to receive(:get).and_return(response_double) + + expect(subject.get(resource)).to eq(response_double.parsed_response) + end + end + + describe 'request query' do + include_examples 'performs network request' do + let(:expected_args) do + [ + anything, + hash_including( + query: { + page: described_class::DEFAULT_PAGE, + per_page: described_class::DEFAULT_PER_PAGE + } + ) + ] + end + end + end + + describe 'request headers' do + include_examples 'performs network request' do + let(:expected_args) do + [ + anything, + hash_including( + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{token}" + } + ) + ] + end + end + end + + describe 'request uri' do + include_examples 'performs network request' do + let(:expected_args) do + ['http://gitlab.example:80/api/v4/resource', anything] + end + end + end + + context 'error handling' do + context 'when error occurred' do + it 'raises ConnectionError' do + allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED) + + expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError) + end + end + + context 'when response is not success' do + it 'raises ConnectionError' do + response_double = double(code: 503, success?: false) + + allow(Gitlab::HTTP).to receive(:get).and_return(response_double) + + expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError) + end + end + end + end +end --- /dev/null +++ gitlab-13.6.5/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# This spec is a lightweight version of: +# * project/tree_restorer_spec.rb +# +# In depth testing is being done in the above specs. +# This spec tests that restore of the sample project works +# but does not have 100% relation coverage. + +require 'spec_helper' + +RSpec.describe Gitlab::ImportExport::Project::Sample::SampleDataRelationTreeRestorer do + include_context 'relation tree restorer shared context' + + let(:sample_data_relation_tree_restorer) do + described_class.new( + user: user, + shared: shared, + relation_reader: relation_reader, + object_builder: object_builder, + members_mapper: members_mapper, + relation_factory: relation_factory, + reader: reader, + importable: importable, + importable_path: importable_path, + importable_attributes: attributes + ) + end + + subject { sample_data_relation_tree_restorer.restore } + + shared_examples 'import project successfully' do + it 'restores project tree' do + expect(subject).to eq(true) + end + + describe 'imported project' do + let(:project) { Project.find_by_path('project') } + + before do + subject + end + + it 'has the project attributes and relations', :aggregate_failures do + expect(project.description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.') + expect(project.issues.count).to eq(10) + expect(project.milestones.count).to eq(3) + expect(project.labels.count).to eq(2) + expect(project.project_feature).not_to be_nil + end + + it 'has issues with correctly updated due dates' do + due_dates = due_dates(project.issues) + + expect(due_dates).to match_array([Date.today - 7.days, Date.today, Date.today + 7.days]) + end + + it 'has milestones with correctly updated due dates' do + due_dates = due_dates(project.milestones) + + expect(due_dates).to match_array([Date.today - 7.days, Date.today, Date.today + 7.days]) + end + + def due_dates(relations) + due_dates = relations.map { |relation| relation['due_date'] } + due_dates.compact! + due_dates.sort + end + end + end + + context 'when restoring a project' do + let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } + let(:importable_name) { 'project' } + let(:importable_path) { 'project' } + let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder } + let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory } + let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } + + context 'using ndjson reader' do + let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' } + let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) } + + it_behaves_like 'import project successfully' + end + end +end --- /dev/null +++ gitlab-13.6.5/spec/lib/gitlab/middleware/handle_null_bytes_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' +require "rack/test" + +RSpec.describe Gitlab::Middleware::HandleNullBytes do + let(:null_byte) { "\u0000" } + let(:error_400) { [400, {}, ["Bad Request"]] } + let(:app) { double(:app) } + + subject { described_class.new(app) } + + before do + allow(app).to receive(:call) do |args| + args + end + end + + def env_for(params = {}) + Rack::MockRequest.env_for('/', { params: params }) + end + + context 'with null bytes in params' do + it 'rejects null bytes in a top level param' do + env = env_for(name: "null#{null_byte}byte") + + expect(subject.call(env)).to eq error_400 + end + + it "responds with 400 BadRequest for hashes with strings" do + env = env_for(name: { inner_key: "I am #{null_byte} bad" }) + + expect(subject.call(env)).to eq error_400 + end + + it "responds with 400 BadRequest for arrays with strings" do + env = env_for(name: ["I am #{null_byte} bad"]) + + expect(subject.call(env)).to eq error_400 + end + + it "responds with 400 BadRequest for arrays containing hashes with string values" do + env = env_for(name: [ + { + inner_key: "I am #{null_byte} bad" + } + ]) + + expect(subject.call(env)).to eq error_400 + end + + it "gives up and does not 400 with too deeply nested params" do + env = env_for(name: [ + { + inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{null_byte} bad" }] } + } + ]) + + expect(subject.call(env)).not_to eq error_400 + end + end + + context 'without null bytes in params' do + it "does not respond with a 400 for strings" do + env = env_for(name: "safe name") + + expect(subject.call(env)).not_to eq error_400 + end + + it "does not respond with a 400 with no params" do + env = env_for + + expect(subject.call(env)).not_to eq error_400 + end + end + + context 'when disabled via env flag' do + before do + stub_env('REJECT_NULL_BYTES', '1') + end + + it 'does not respond with a 400 no matter what' do + env = env_for(name: "null#{null_byte}byte") + + expect(subject.call(env)).not_to eq error_400 + end + end +end --- /dev/null +++ gitlab-13.6.5/spec/migrations/schedule_blocked_by_links_replacement_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20201015073808_schedule_blocked_by_links_replacement') + +RSpec.describe ScheduleBlockedByLinksReplacement do + let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { table(:projects).create!(namespace_id: namespace.id, name: 'gitlab') } + let(:issue1) { table(:issues).create!(project_id: project.id, title: 'a') } + let(:issue2) { table(:issues).create!(project_id: project.id, title: 'b') } + let(:issue3) { table(:issues).create!(project_id: project.id, title: 'c') } + let!(:issue_links) do + [ + table(:issue_links).create!(source_id: issue1.id, target_id: issue2.id, link_type: 1), + table(:issue_links).create!(source_id: issue2.id, target_id: issue1.id, link_type: 2), + table(:issue_links).create!(source_id: issue1.id, target_id: issue3.id, link_type: 2) + ] + end + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + end + + it 'schedules jobs for blocked_by links' do + Sidekiq::Testing.fake! do + freeze_time do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration( + 2.minutes, issue_links[1].id, issue_links[1].id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration( + 4.minutes, issue_links[2].id, issue_links[2].id) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end +end --- /dev/null +++ gitlab-13.6.5/spec/models/ci/build_trace_chunks/legacy_fog_spec.rb @@ -0,0 +1,164 @@ +# 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 --- /dev/null +++ gitlab-13.6.5/spec/requests/user_sends_null_bytes_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'User sends null bytes as params' do + let(:null_byte) { "\u0000" } + + it 'raises a 400 error' do + post '/nonexistent', params: { a: "A #{null_byte} nasty string" } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to eq('Bad Request') + end +end