Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-24 21:07:55 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-24 21:07:55 +0300
commit603c7d4cac5e28bc1c75e50c23ed2cbe56f1aafc (patch)
tree907f5b8ee1b6f5aad396e95e3327a08400b9e8ea /spec
parent120f4aaedc8fe830a3f572491d240d8ee6addefb (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/services_controller_spec.rb19
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb37
-rw-r--r--spec/features/milestones/user_views_milestone_spec.rb31
-rw-r--r--spec/features/projects/environments_pod_logs_spec.rb6
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js1
-rw-r--r--spec/frontend/code_navigation/components/popover_spec.js16
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js38
-rw-r--r--spec/frontend/code_navigation/store/mutations_spec.js10
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js2
-rw-r--r--spec/frontend/logs/components/environment_logs_spec.js112
-rw-r--r--spec/frontend/logs/components/log_advanced_filters_spec.js185
-rw-r--r--spec/frontend/logs/components/log_simple_filters_spec.js138
-rw-r--r--spec/frontend/logs/stores/getters_spec.js45
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb1
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb28
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb42
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb16
-rw-r--r--spec/models/ci/job_artifact_spec.rb12
-rw-r--r--spec/models/concerns/milestoneish_spec.rb19
-rw-r--r--spec/models/service_spec.rb51
-rw-r--r--spec/services/ci/retry_build_service_spec.rb1
-rw-r--r--spec/support/shared_examples/uploaders/upload_type_shared_examples.rb12
-rw-r--r--spec/uploaders/content_type_whitelist_spec.rb1
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb7
24 files changed, 664 insertions, 166 deletions
diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb
index 35801643181..5dde0d57293 100644
--- a/spec/controllers/admin/services_controller_spec.rb
+++ b/spec/controllers/admin/services_controller_spec.rb
@@ -10,21 +10,14 @@ describe Admin::ServicesController do
end
describe 'GET #edit' do
- let!(:project) { create(:project) }
-
- Service.available_services_names.each do |service_name|
- context "#{service_name}" do
- let!(:service) do
- service_template = "#{service_name}_service".camelize.constantize
- service_template.where(template: true).first_or_create
- end
+ let!(:service) do
+ create(:jira_service, :template)
+ end
- it 'successfully displays the template' do
- get :edit, params: { id: service.id }
+ it 'successfully displays the template' do
+ get :edit, params: { id: service.id }
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
+ expect(response).to have_gitlab_http_status(:ok)
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 225538dcc45..9fdaa728fd7 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -8,18 +8,17 @@ describe Projects::BlobController do
let(:project) { create(:project, :public, :repository) }
describe "GET show" do
+ def request
+ get(:show, params: { namespace_id: project.namespace, project_id: project, id: id })
+ end
+
render_views
context 'with file path' do
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id
- })
+ request
end
context "valid branch, valid file" do
@@ -119,6 +118,32 @@ describe Projects::BlobController do
end
end
end
+
+ context 'when there is an artifact with code navigation data' do
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
+ let!(:job) { create(:ci_build, pipeline: pipeline, name: Ci::Build::CODE_NAVIGATION_JOB_NAME) }
+ let!(:artifact) { create(:ci_job_artifact, :lsif, job: job) }
+
+ let(:id) { 'master/README.md' }
+
+ it 'assigns code_navigation_build variable' do
+ request
+
+ expect(assigns[:code_navigation_build]).to eq(job)
+ end
+
+ context 'when code_navigation feature is disabled' do
+ before do
+ stub_feature_flags(code_navigation: false)
+ end
+
+ it 'does not assign code_navigation_build variable' do
+ request
+
+ expect(assigns[:code_navigation_build]).to be_nil
+ end
+ end
+ end
end
describe 'GET diff' do
diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb
index cbc21dd02e5..d8bb4902087 100644
--- a/spec/features/milestones/user_views_milestone_spec.rb
+++ b/spec/features/milestones/user_views_milestone_spec.rb
@@ -25,6 +25,37 @@ describe "User views milestone" do
expect { visit_milestone }.not_to exceed_query_limit(control)
end
+ context 'limiting milestone issues' do
+ before_all do
+ 2.times do
+ create(:issue, milestone: milestone, project: project)
+ create(:issue, milestone: milestone, project: project, assignees: [user])
+ create(:issue, milestone: milestone, project: project, state: :closed)
+ end
+ end
+
+ context 'when issues on milestone are over DISPLAY_ISSUES_LIMIT' do
+ it "limits issues to display and shows warning" do
+ stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 3)
+
+ visit(project_milestone_path(project, milestone))
+
+ expect(page).to have_selector('.issuable-row', count: 3)
+ expect(page).to have_selector('#milestone-issue-count-warning', text: 'Showing 3 of 6 issues. View all issues')
+ expect(page).to have_link('View all issues', href: project_issues_path(project, { milestone_title: milestone.title }))
+ end
+ end
+
+ context 'when issues on milestone are below DISPLAY_ISSUES_LIMIT' do
+ it 'does not display warning' do
+ visit(project_milestone_path(project, milestone))
+
+ expect(page).not_to have_selector('#milestone-issue-count-warning', text: 'Showing 3 of 6 issues. View all issues')
+ expect(page).to have_selector('.issuable-row', count: 6)
+ end
+ end
+ end
+
private
def visit_milestone
diff --git a/spec/features/projects/environments_pod_logs_spec.rb b/spec/features/projects/environments_pod_logs_spec.rb
index 121a8e1705b..2b2327940a5 100644
--- a/spec/features/projects/environments_pod_logs_spec.rb
+++ b/spec/features/projects/environments_pod_logs_spec.rb
@@ -29,7 +29,7 @@ describe 'Environment > Pod Logs', :js do
wait_for_requests
page.within('.js-environments-dropdown') do
- toggle = find(".dropdown-menu-toggle:not([disabled])")
+ toggle = find(".dropdown-toggle:not([disabled])")
expect(toggle).to have_content(environment.name)
@@ -47,8 +47,8 @@ describe 'Environment > Pod Logs', :js do
wait_for_requests
- page.within('.js-pods-dropdown') do
- find(".dropdown-menu-toggle:not([disabled])").click
+ page.within('.qa-pods-dropdown') do
+ find(".dropdown-toggle:not([disabled])").click
dropdown_items = find(".dropdown-menu").all(".dropdown-item:not([disabled])")
expect(dropdown_items.size).to eq(1)
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index cfdc0dcc6cc..d5693cc4173 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -16,6 +16,7 @@ function factory(initialState = {}) {
state: {
...createState(),
...initialState,
+ definitionPathPrefix: 'https://test.com/blob/master',
},
actions: {
fetchData,
diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js
index ad05504a224..df3bbc7c1c6 100644
--- a/spec/frontend/code_navigation/components/popover_spec.js
+++ b/spec/frontend/code_navigation/components/popover_spec.js
@@ -1,6 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import Popover from '~/code_navigation/components/popover.vue';
+const DEFINITION_PATH_PREFIX = 'http:/';
+
const MOCK_CODE_DATA = Object.freeze({
hover: [
{
@@ -8,7 +10,7 @@ const MOCK_CODE_DATA = Object.freeze({
value: 'console.log',
},
],
- definition_url: 'http://test.com',
+ definition_path: 'test.com',
});
const MOCK_DOCS_DATA = Object.freeze({
@@ -18,13 +20,13 @@ const MOCK_DOCS_DATA = Object.freeze({
value: 'console.log',
},
],
- definition_url: 'http://test.com',
+ definition_path: 'test.com',
});
let wrapper;
-function factory(position, data) {
- wrapper = shallowMount(Popover, { propsData: { position, data } });
+function factory(position, data, definitionPathPrefix) {
+ wrapper = shallowMount(Popover, { propsData: { position, data, definitionPathPrefix } });
}
describe('Code navigation popover component', () => {
@@ -33,14 +35,14 @@ describe('Code navigation popover component', () => {
});
it('renders popover', () => {
- factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA);
+ factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.element).toMatchSnapshot();
});
describe('code output', () => {
it('renders code output', () => {
- factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA);
+ factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false);
@@ -49,7 +51,7 @@ describe('Code navigation popover component', () => {
describe('documentation output', () => {
it('renders code output', () => {
- factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA);
+ factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true);
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index 9c44480ca67..f58dc283ada 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -27,12 +27,10 @@ describe('Code navigation actions', () => {
describe('fetchData', () => {
let mock;
- const state = {
- projectPath: 'gitlab-org/gitlab',
- commitId: '123',
- blobPath: 'index',
- };
- const apiUrl = '/api/1/projects/gitlab-org%2Fgitlab/commits/123/lsif/info';
+
+ const codeNavUrl =
+ 'gitlab-org/gitlab-shell/-/jobs/1114/artifacts/raw/lsif/cmd/check/main.go.json';
+ const state = { codeNavUrl };
beforeEach(() => {
window.gon = { api_version: '1' };
@@ -45,20 +43,18 @@ describe('Code navigation actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(apiUrl).replyOnce(200, {
- index: [
- {
- start_line: 0,
- start_char: 0,
- hover: { value: '123' },
- },
- {
- start_line: 1,
- start_char: 0,
- hover: null,
- },
- ],
- });
+ mock.onGet(codeNavUrl).replyOnce(200, [
+ {
+ start_line: 0,
+ start_char: 0,
+ hover: { value: '123' },
+ },
+ {
+ start_line: 1,
+ start_char: 0,
+ hover: null,
+ },
+ ]);
});
it('commits REQUEST_DATA_SUCCESS with normalized data', done => {
@@ -106,7 +102,7 @@ describe('Code navigation actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(apiUrl).replyOnce(500);
+ mock.onGet(codeNavUrl).replyOnce(500);
});
it('dispatches requestDataError', done => {
diff --git a/spec/frontend/code_navigation/store/mutations_spec.js b/spec/frontend/code_navigation/store/mutations_spec.js
index 117a2ed2f14..305386f4d0b 100644
--- a/spec/frontend/code_navigation/store/mutations_spec.js
+++ b/spec/frontend/code_navigation/store/mutations_spec.js
@@ -11,14 +11,12 @@ describe('Code navigation mutations', () => {
describe('SET_INITIAL_DATA', () => {
it('sets initial data', () => {
mutations.SET_INITIAL_DATA(state, {
- projectPath: 'test',
- commitId: '123',
- blobPath: 'index.js',
+ codeNavUrl: 'https://test.com/builds/1005',
+ definitionPathPrefix: 'https://test.com/blob/master',
});
- expect(state.projectPath).toBe('test');
- expect(state.commitId).toBe('123');
- expect(state.blobPath).toBe('index.js');
+ expect(state.codeNavUrl).toBe('https://test.com/builds/1005');
+ expect(state.definitionPathPrefix).toBe('https://test.com/blob/master');
});
});
diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js
index fe142d70698..826d51b24f1 100644
--- a/spec/frontend/ide/components/branches/search_list_spec.js
+++ b/spec/frontend/ide/components/branches/search_list_spec.js
@@ -9,6 +9,8 @@ import { branches } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
+jest.mock('lodash/debounce', () => jest.fn);
+
describe('IDE branches search list', () => {
let wrapper;
const fetchBranchesMock = jest.fn();
diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js
index 49642153c69..4da987725a1 100644
--- a/spec/frontend/logs/components/environment_logs_spec.js
+++ b/spec/frontend/logs/components/environment_logs_spec.js
@@ -1,7 +1,5 @@
-import Vue from 'vue';
-import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui';
+import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import EnvironmentLogs from '~/logs/components/environment_logs.vue';
import { createStore } from '~/logs/stores';
@@ -13,7 +11,6 @@ import {
mockLogsResult,
mockTrace,
mockPodName,
- mockSearch,
mockEnvironmentsEndpoint,
mockDocumentationPath,
} from '../mock_data';
@@ -29,7 +26,6 @@ jest.mock('lodash/throttle', () =>
);
describe('EnvironmentLogs', () => {
- let EnvironmentLogsComponent;
let store;
let dispatch;
let wrapper;
@@ -44,13 +40,9 @@ describe('EnvironmentLogs', () => {
const updateControlBtnsMock = jest.fn();
const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown');
- const findPodsDropdown = () => wrapper.find('.js-pods-dropdown');
- const findPodsDropdownItems = () =>
- findPodsDropdown()
- .findAll(GlDropdownItem)
- .filter(itm => !itm.attributes('disabled'));
- const findSearchBar = () => wrapper.find('.js-logs-search');
- const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
+
+ const findSimpleFilters = () => wrapper.find({ ref: 'log-simple-filters' });
+ const findAdvancedFilters = () => wrapper.find({ ref: 'log-advanced-filters' });
const findInfoAlert = () => wrapper.find('.js-elasticsearch-alert');
const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
@@ -79,7 +71,7 @@ describe('EnvironmentLogs', () => {
};
const initWrapper = () => {
- wrapper = shallowMount(EnvironmentLogsComponent, {
+ wrapper = shallowMount(EnvironmentLogs, {
propsData,
store,
stubs: {
@@ -111,7 +103,6 @@ describe('EnvironmentLogs', () => {
beforeEach(() => {
store = createStore();
state = store.state.environmentLogs;
- EnvironmentLogsComponent = Vue.extend(EnvironmentLogs);
jest.spyOn(store, 'dispatch').mockResolvedValue();
@@ -132,17 +123,10 @@ describe('EnvironmentLogs', () => {
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
- // top bar
expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true);
- expect(findPodsDropdown().is(GlDropdown)).toBe(true);
+ expect(findSimpleFilters().exists()).toBe(true);
expect(findLogControlButtons().exists()).toBe(true);
- expect(findSearchBar().exists()).toBe(true);
- expect(findSearchBar().is(GlSearchBoxByClick)).toBe(true);
- expect(findTimeRangePicker().exists()).toBe(true);
- expect(findTimeRangePicker().is(DateTimePicker)).toBe(true);
-
- // log trace
expect(findInfiniteScroll().exists()).toBe(true);
expect(findLogTrace().exists()).toBe(true);
});
@@ -181,20 +165,6 @@ describe('EnvironmentLogs', () => {
expect(findEnvironmentsDropdown().findAll(GlDropdownItem).length).toBe(0);
});
- it('displays a disabled pods dropdown', () => {
- expect(findPodsDropdown().attributes('disabled')).toBe('true');
- expect(findPodsDropdownItems()).toHaveLength(0);
- });
-
- it('displays a disabled search bar', () => {
- expect(findSearchBar().exists()).toBe(true);
- expect(findSearchBar().attributes('disabled')).toBe('true');
- });
-
- it('displays a disabled time window dropdown', () => {
- expect(findTimeRangePicker().attributes('disabled')).toBe('true');
- });
-
it('does not update buttons state', () => {
expect(updateControlBtnsMock).not.toHaveBeenCalled();
});
@@ -237,17 +207,14 @@ describe('EnvironmentLogs', () => {
initWrapper();
});
- it('displays a disabled time window dropdown', () => {
- expect(findTimeRangePicker().attributes('disabled')).toBe('true');
- });
-
- it('displays a disabled search bar', () => {
- expect(findSearchBar().attributes('disabled')).toBe('true');
- });
-
it('displays an alert to upgrade to ES', () => {
expect(findInfoAlert().exists()).toBe(true);
});
+
+ it('displays simple filters for kubernetes logs API', () => {
+ expect(findSimpleFilters().exists()).toBe(true);
+ expect(findAdvancedFilters().exists()).toBe(false);
+ });
});
describe('state with data', () => {
@@ -271,21 +238,6 @@ describe('EnvironmentLogs', () => {
updateControlBtnsMock.mockReset();
});
- it('displays an enabled search bar', () => {
- expect(findSearchBar().attributes('disabled')).toBeFalsy();
-
- // input a query and click `search`
- findSearchBar().vm.$emit('input', mockSearch);
- findSearchBar().vm.$emit('submit');
-
- expect(dispatch).toHaveBeenCalledWith(`${module}/setInitData`, expect.any(Object));
- expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch);
- });
-
- it('displays an enabled time window dropdown', () => {
- expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
- });
-
it('does not display an alert to upgrade to ES', () => {
expect(findInfoAlert().exists()).toBe(false);
});
@@ -306,24 +258,16 @@ describe('EnvironmentLogs', () => {
const item = items.at(i);
if (item.text() !== mockEnvName) {
- expect(item.find(GlIcon).classes()).toContain('invisible');
+ expect(item.find(GlIcon).classes('invisible')).toBe(true);
} else {
- // selected
- expect(item.find(GlIcon).classes()).not.toContain('invisible');
+ expect(item.find(GlIcon).classes('invisible')).toBe(false);
}
});
});
- it('populates pods dropdown', () => {
- const items = findPodsDropdownItems();
-
- expect(findPodsDropdown().props('text')).toBe(mockPodName);
- expect(items.length).toBe(mockPods.length + 1);
- expect(items.at(0).text()).toBe('All pods');
- mockPods.forEach((pod, i) => {
- const item = items.at(i + 1);
- expect(item.text()).toBe(pod);
- });
+ it('displays advanced filters for elasticsearch logs API', () => {
+ expect(findSimpleFilters().exists()).toBe(false);
+ expect(findAdvancedFilters().exists()).toBe(true);
});
it('shows infinite scroll with height and no content', () => {
@@ -331,19 +275,6 @@ describe('EnvironmentLogs', () => {
expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length);
});
- it('dropdown has one pod selected', () => {
- const items = findPodsDropdownItems();
- mockPods.forEach((pod, i) => {
- const item = items.at(i);
- if (item.text() !== mockPodName) {
- expect(item.find(GlIcon).classes()).toContain('invisible');
- } else {
- // selected
- expect(item.find(GlIcon).classes()).not.toContain('invisible');
- }
- });
- });
-
it('populates logs trace', () => {
const trace = findLogTrace();
expect(trace.text().split('\n').length).toBe(mockTrace.length);
@@ -371,17 +302,6 @@ describe('EnvironmentLogs', () => {
);
});
- it('pod name, trace is refreshed', () => {
- const items = findPodsDropdownItems();
- const index = 2; // any pod
-
- expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
-
- items.at(index + 1).vm.$emit('click');
-
- expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
- });
-
it('refresh button, trace is refreshed', () => {
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
diff --git a/spec/frontend/logs/components/log_advanced_filters_spec.js b/spec/frontend/logs/components/log_advanced_filters_spec.js
new file mode 100644
index 00000000000..a6fbc40c2c6
--- /dev/null
+++ b/spec/frontend/logs/components/log_advanced_filters_spec.js
@@ -0,0 +1,185 @@
+import { GlIcon, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { defaultTimeRange } from '~/vue_shared/constants';
+import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import { createStore } from '~/logs/stores';
+import { mockPods, mockSearch } from '../mock_data';
+
+import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue';
+
+const module = 'environmentLogs';
+
+describe('LogAdvancedFilters', () => {
+ let store;
+ let dispatch;
+ let wrapper;
+ let state;
+
+ const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' });
+ const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
+ const findPodsDropdownItems = () =>
+ findPodsDropdown()
+ .findAll(GlDropdownItem)
+ .filter(item => !item.is('[disabled]'));
+ const findPodsDropdownItemsSelected = () =>
+ findPodsDropdownItems()
+ .filter(item => {
+ return !item.find(GlIcon).classes('invisible');
+ })
+ .at(0);
+ const findSearchBox = () => wrapper.find({ ref: 'searchBox' });
+ const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
+
+ const mockStateLoading = () => {
+ state.timeRange.selected = defaultTimeRange;
+ state.timeRange.current = convertToFixedRange(defaultTimeRange);
+ state.pods.options = [];
+ state.pods.current = null;
+ };
+
+ const mockStateWithData = () => {
+ state.timeRange.selected = defaultTimeRange;
+ state.timeRange.current = convertToFixedRange(defaultTimeRange);
+ state.pods.options = mockPods;
+ state.pods.current = null;
+ };
+
+ const initWrapper = (propsData = {}) => {
+ wrapper = shallowMount(LogAdvancedFilters, {
+ propsData: {
+ ...propsData,
+ },
+ store,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ state = store.state.environmentLogs;
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+
+ dispatch = store.dispatch;
+ });
+
+ afterEach(() => {
+ store.dispatch.mockReset();
+
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ it('displays UI elements', () => {
+ initWrapper();
+
+ expect(wrapper.isVueInstance()).toBe(true);
+ expect(wrapper.isEmpty()).toBe(false);
+
+ expect(findPodsDropdown().exists()).toBe(true);
+ expect(findSearchBox().exists()).toBe(true);
+ expect(findTimeRangePicker().exists()).toBe(true);
+ });
+
+ describe('disabled state', () => {
+ beforeEach(() => {
+ mockStateLoading();
+ initWrapper({
+ disabled: true,
+ });
+ });
+
+ it('displays disabled filters', () => {
+ expect(findPodsDropdown().props('text')).toBe('All pods');
+ expect(findPodsDropdown().attributes('disabled')).toBeTruthy();
+ expect(findSearchBox().attributes('disabled')).toBeTruthy();
+ expect(findTimeRangePicker().attributes('disabled')).toBeTruthy();
+ });
+ });
+
+ describe('when the state is loading', () => {
+ beforeEach(() => {
+ mockStateLoading();
+ initWrapper();
+ });
+
+ it('displays a enabled filters', () => {
+ expect(findPodsDropdown().props('text')).toBe('All pods');
+ expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
+ expect(findSearchBox().attributes('disabled')).toBeFalsy();
+ expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
+ });
+
+ it('displays an empty pods dropdown', () => {
+ expect(findPodsNoPodsText().exists()).toBe(true);
+ expect(findPodsDropdownItems()).toHaveLength(0);
+ });
+ });
+
+ describe('when the state has data', () => {
+ beforeEach(() => {
+ mockStateWithData();
+ initWrapper();
+ });
+
+ it('displays an enabled pods dropdown', () => {
+ expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
+ expect(findPodsDropdown().props('text')).toBe('All pods');
+ });
+
+ it('displays options in a pods dropdown', () => {
+ const items = findPodsDropdownItems();
+ expect(items).toHaveLength(mockPods.length + 1);
+ });
+
+ it('displays "all pods" selected in a pods dropdown', () => {
+ const selected = findPodsDropdownItemsSelected();
+
+ expect(selected.text()).toBe('All pods');
+ });
+
+ it('displays options in date time picker', () => {
+ const options = findTimeRangePicker().props('options');
+
+ expect(options).toEqual(expect.any(Array));
+ expect(options.length).toBeGreaterThan(0);
+ });
+
+ describe('when the user interacts', () => {
+ it('clicks on a all options, showPodLogs is dispatched with null', () => {
+ const items = findPodsDropdownItems();
+ items.at(0).vm.$emit('click');
+
+ expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, null);
+ });
+
+ it('clicks on a pod name, showPodLogs is dispatched with pod name', () => {
+ const items = findPodsDropdownItems();
+ const index = 2; // any pod
+
+ items.at(index + 1).vm.$emit('click'); // skip "All pods" option
+
+ expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
+ });
+
+ it('clicks on search, a serches is done', () => {
+ expect(findSearchBox().attributes('disabled')).toBeFalsy();
+
+ // input a query and click `search`
+ findSearchBox().vm.$emit('input', mockSearch);
+ findSearchBox().vm.$emit('submit');
+
+ expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch);
+ });
+
+ it('selects a new time range', () => {
+ expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
+
+ const mockRange = { start: 'START_DATE', end: 'END_DATE' };
+ findTimeRangePicker().vm.$emit('input', mockRange);
+
+ expect(dispatch).toHaveBeenCalledWith(`${module}/setTimeRange`, mockRange);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js
new file mode 100644
index 00000000000..13504a2b1fc
--- /dev/null
+++ b/spec/frontend/logs/components/log_simple_filters_spec.js
@@ -0,0 +1,138 @@
+import { GlIcon, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/logs/stores';
+import { mockPods, mockPodName } from '../mock_data';
+
+import LogSimpleFilters from '~/logs/components/log_simple_filters.vue';
+
+const module = 'environmentLogs';
+
+describe('LogSimpleFilters', () => {
+ let store;
+ let dispatch;
+ let wrapper;
+ let state;
+
+ const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' });
+ const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
+ const findPodsDropdownItems = () =>
+ findPodsDropdown()
+ .findAll(GlDropdownItem)
+ .filter(item => !item.is('[disabled]'));
+
+ const mockPodsLoading = () => {
+ state.pods.options = [];
+ state.pods.current = null;
+ };
+
+ const mockPodsLoaded = () => {
+ state.pods.options = mockPods;
+ state.pods.current = mockPodName;
+ };
+
+ const initWrapper = (propsData = {}) => {
+ wrapper = shallowMount(LogSimpleFilters, {
+ propsData: {
+ ...propsData,
+ },
+ store,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ state = store.state.environmentLogs;
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+
+ dispatch = store.dispatch;
+ });
+
+ afterEach(() => {
+ store.dispatch.mockReset();
+
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ it('displays UI elements', () => {
+ initWrapper();
+
+ expect(wrapper.isVueInstance()).toBe(true);
+ expect(wrapper.isEmpty()).toBe(false);
+
+ expect(findPodsDropdown().exists()).toBe(true);
+ });
+
+ describe('disabled state', () => {
+ beforeEach(() => {
+ mockPodsLoading();
+ initWrapper({
+ disabled: true,
+ });
+ });
+
+ it('displays a disabled pods dropdown', () => {
+ expect(findPodsDropdown().props('text')).toBe('No pod selected');
+ expect(findPodsDropdown().attributes('disabled')).toBeTruthy();
+ });
+ });
+
+ describe('loading state', () => {
+ beforeEach(() => {
+ mockPodsLoading();
+ initWrapper();
+ });
+
+ it('displays an enabled pods dropdown', () => {
+ expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
+ expect(findPodsDropdown().props('text')).toBe('No pod selected');
+ });
+
+ it('displays an empty pods dropdown', () => {
+ expect(findPodsNoPodsText().exists()).toBe(true);
+ expect(findPodsDropdownItems()).toHaveLength(0);
+ });
+ });
+
+ describe('pods available state', () => {
+ beforeEach(() => {
+ mockPodsLoaded();
+ initWrapper();
+ });
+
+ it('displays an enabled pods dropdown', () => {
+ expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
+ expect(findPodsDropdown().props('text')).toBe(mockPods[0]);
+ });
+
+ it('displays a pods dropdown with items', () => {
+ expect(findPodsNoPodsText().exists()).toBe(false);
+ expect(findPodsDropdownItems()).toHaveLength(mockPods.length);
+ });
+
+ it('dropdown has one pod selected', () => {
+ const items = findPodsDropdownItems();
+ mockPods.forEach((pod, i) => {
+ const item = items.at(i);
+ if (item.text() !== mockPodName) {
+ expect(item.find(GlIcon).classes('invisible')).toBe(true);
+ } else {
+ expect(item.find(GlIcon).classes('invisible')).toBe(false);
+ }
+ });
+ });
+
+ it('when the user clicks on a pod, showPodLogs is dispatched', () => {
+ const items = findPodsDropdownItems();
+ const index = 2; // any pod
+
+ expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
+
+ items.at(index).vm.$emit('click');
+
+ expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
+ });
+ });
+});
diff --git a/spec/frontend/logs/stores/getters_spec.js b/spec/frontend/logs/stores/getters_spec.js
index fdce575fa97..9d213d8c01f 100644
--- a/spec/frontend/logs/stores/getters_spec.js
+++ b/spec/frontend/logs/stores/getters_spec.js
@@ -1,7 +1,7 @@
-import * as getters from '~/logs/stores/getters';
+import { trace, showAdvancedFilters } from '~/logs/stores/getters';
import logsPageState from '~/logs/stores/state';
-import { mockLogsResult, mockTrace } from '../mock_data';
+import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data';
describe('Logs Store getters', () => {
let state;
@@ -13,7 +13,7 @@ describe('Logs Store getters', () => {
describe('trace', () => {
describe('when state is initialized', () => {
it('returns an empty string', () => {
- expect(getters.trace(state)).toEqual('');
+ expect(trace(state)).toEqual('');
});
});
@@ -23,7 +23,7 @@ describe('Logs Store getters', () => {
});
it('returns an empty string', () => {
- expect(getters.trace(state)).toEqual('');
+ expect(trace(state)).toEqual('');
});
});
@@ -33,7 +33,42 @@ describe('Logs Store getters', () => {
});
it('returns an empty string', () => {
- expect(getters.trace(state)).toEqual(mockTrace.join('\n'));
+ expect(trace(state)).toEqual(mockTrace.join('\n'));
+ });
+ });
+ });
+
+ describe('showAdvancedFilters', () => {
+ describe('when no environments are set', () => {
+ beforeEach(() => {
+ state.environments.current = mockEnvName;
+ state.environments.options = [];
+ });
+
+ it('returns false', () => {
+ expect(showAdvancedFilters(state)).toBe(false);
+ });
+ });
+
+ describe('when the environment supports filters', () => {
+ beforeEach(() => {
+ state.environments.current = mockEnvName;
+ state.environments.options = mockEnvironments;
+ });
+
+ it('returns true', () => {
+ expect(showAdvancedFilters(state)).toBe(true);
+ });
+ });
+
+ describe('when the environment does not support filters', () => {
+ beforeEach(() => {
+ state.environments.options = mockEnvironments;
+ state.environments.current = mockEnvironments[1].name;
+ });
+
+ it('returns true', () => {
+ expect(showAdvancedFilters(state)).toBe(false);
});
});
});
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 2c8f76c8f34..9bba3eb2b77 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -46,6 +46,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
:lsif | 'lsif.json'
:dotenv | 'build.dotenv'
:cobertura | 'cobertura-coverage.xml'
+ :terraform | 'tfplan.json'
end
with_them do
diff --git a/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
new file mode 100644
index 00000000000..6f20b8877e0
--- /dev/null
+++ b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::GrapeLogging::Loggers::PerfLogger do
+ subject { described_class.new }
+
+ describe ".parameters" do
+ let(:mock_request) { OpenStruct.new(env: {}) }
+
+ describe 'when no performance datais are present' do
+ it 'returns an empty Hash' do
+ expect(subject.parameters(mock_request, nil)).to eq({})
+ end
+ end
+
+ describe 'when Redis calls are present', :request_store do
+ it 'returns a Hash with Redis information' do
+ Gitlab::Redis::SharedState.with { |redis| redis.get('perf-logger-test') }
+
+ payload = subject.parameters(mock_request, nil)
+
+ expect(payload[:redis_calls]).to eq(1)
+ expect(payload[:redis_duration_ms]).to be >= 0
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index c2674638743..9788c9f4a3c 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -1,11 +1,51 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
require 'rspec-parameterized'
describe Gitlab::InstrumentationHelper do
using RSpec::Parameterized::TableSyntax
+ describe '.add_instrumentation_data', :request_store do
+ let(:payload) { {} }
+
+ subject { described_class.add_instrumentation_data(payload) }
+
+ it 'adds nothing' do
+ subject
+
+ expect(payload).to eq({})
+ end
+
+ context 'when Gitaly calls are made' do
+ it 'adds Gitaly data and omits Redis data' do
+ project = create(:project)
+ RequestStore.clear!
+ project.repository.exists?
+
+ subject
+
+ expect(payload[:gitaly_calls]).to eq(1)
+ expect(payload[:gitaly_duration]).to be >= 0
+ expect(payload[:redis_calls]).to be_nil
+ expect(payload[:redis_duration_ms]).to be_nil
+ end
+ end
+
+ context 'when Redis calls are made' do
+ it 'adds Redis data and omits Gitaly data' do
+ Gitlab::Redis::Cache.with { |redis| redis.get('test-instrumentation') }
+
+ subject
+
+ expect(payload[:redis_calls]).to eq(1)
+ expect(payload[:redis_duration_ms]).to be >= 0
+ expect(payload[:gitaly_calls]).to be_nil
+ expect(payload[:gitaly_duration]).to be_nil
+ end
+ end
+ end
+
describe '.queue_duration_for_job' do
where(:enqueued_at, :created_at, :time_now, :expected_duration) do
"2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index bd04d30f85f..aab63ba88ad 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -175,26 +175,30 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
- context 'with Gitaly and Rugged calls' do
+ context 'with Gitaly, Rugged, and Redis calls' do
let(:timing_data) do
{
gitaly_calls: 10,
gitaly_duration: 10000,
rugged_calls: 1,
- rugged_duration_ms: 5000
+ rugged_duration_ms: 5000,
+ redis_calls: 3,
+ redis_duration_ms: 1234
}
end
- before do
- job.merge!(timing_data)
+ let(:expected_end_payload) do
+ end_payload.except('args').merge(timing_data)
end
it 'logs with Gitaly and Rugged timing data' do
Timecop.freeze(timestamp) do
expect(logger).to receive(:info).with(start_payload.except('args')).ordered
- expect(logger).to receive(:info).with(end_payload.except('args')).ordered
+ expect(logger).to receive(:info).with(expected_end_payload).ordered
- subject.call(job, 'test_queue') { }
+ subject.call(job, 'test_queue') do
+ job.merge!(timing_data)
+ end
end
end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index de93c3c1675..6f6ff3704b4 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -140,6 +140,18 @@ describe Ci::JobArtifact do
end
end
+ describe '.for_job_name' do
+ it 'returns job artifacts for a given job name' do
+ first_job = create(:ci_build, name: 'first')
+ second_job = create(:ci_build, name: 'second')
+ first_artifact = create(:ci_job_artifact, job: first_job)
+ second_artifact = create(:ci_job_artifact, job: second_job)
+
+ expect(described_class.for_job_name(first_job.name)).to eq([first_artifact])
+ expect(described_class.for_job_name(second_job.name)).to eq([second_artifact])
+ end
+ end
+
describe 'callbacks' do
subject { create(:ci_job_artifact, :archive) }
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index cff607a4731..5808d6e37e5 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -33,17 +33,34 @@ describe Milestone, 'Milestoneish' do
end
describe '#sorted_issues' do
- it 'sorts issues by label priority' do
+ before do
issue.labels << label_1
security_issue_1.labels << label_2
closed_issue_1.labels << label_3
+ end
+ it 'sorts issues by label priority' do
issues = milestone.sorted_issues(member)
expect(issues.first).to eq(issue)
expect(issues.second).to eq(security_issue_1)
expect(issues.third).not_to eq(closed_issue_1)
end
+
+ it 'limits issue count and keeps the ordering' do
+ stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 4)
+
+ issues = milestone.sorted_issues(member)
+ # Cannot use issues.count here because it is sorting
+ # by a virtual column 'highest_priority' and it will break
+ # the query.
+ total_issues_count = issues.opened.unassigned.length + issues.opened.assigned.length + issues.closed.length
+ expect(issues.length).to eq(4)
+ expect(total_issues_count).to eq(4)
+ expect(issues.first).to eq(issue)
+ expect(issues.second).to eq(security_issue_1)
+ expect(issues.third).not_to eq(closed_issue_1)
+ end
end
context 'attributes visibility' do
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 7c80b5231d1..cecd4f76fc5 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -149,9 +149,58 @@ describe Service do
end
end
- describe "Template" do
+ describe 'template' do
let(:project) { create(:project) }
+ shared_examples 'retrieves service templates' do
+ it 'returns the available service templates' do
+ expect(Service.find_or_create_templates.pluck(:type)).to match_array(Service.available_services_types)
+ end
+ end
+
+ describe '.find_or_create_templates' do
+ it 'creates service templates' do
+ expect { Service.find_or_create_templates }.to change { Service.count }.from(0).to(Service.available_services_names.size)
+ end
+
+ it_behaves_like 'retrieves service templates'
+
+ context 'with all existing templates' do
+ before do
+ Service.insert_all(
+ Service.available_services_types.map { |type| { template: true, type: type } }
+ )
+ end
+
+ it 'does not create service templates' do
+ expect { Service.find_or_create_templates }.to change { Service.count }.by(0)
+ end
+
+ it_behaves_like 'retrieves service templates'
+
+ context 'with a previous existing service (Previous) and a new service (Asana)' do
+ before do
+ Service.insert(type: 'PreviousService', template: true)
+ Service.delete_by(type: 'AsanaService', template: true)
+ end
+
+ it_behaves_like 'retrieves service templates'
+ end
+ end
+
+ context 'with a few existing templates' do
+ before do
+ create(:jira_service, :template)
+ end
+
+ it 'creates the rest of the service templates' do
+ expect { Service.find_or_create_templates }.to change { Service.count }.from(1).to(Service.available_services_names.size)
+ end
+
+ it_behaves_like 'retrieves service templates'
+ end
+ end
+
describe '.build_from_template' do
context 'when template is invalid' do
it 'sets service template to inactive when template is invalid' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 0ed4dcec93e..86b68dc3ade 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -34,6 +34,7 @@ describe Ci::RetryBuildService do
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_license_scanning
job_artifacts_performance job_artifacts_lsif
+ job_artifacts_terraform
job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv
diff --git a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb
index 81ac6bd94db..e58723324d3 100644
--- a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb
@@ -26,3 +26,15 @@ shared_examples 'accepted carrierwave upload' do
expect { uploader.cache!(fixture_file) }.to change { uploader.file }.from(nil).to(kind_of(CarrierWave::SanitizedFile))
end
end
+
+# @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg')
+# @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload.
+# @param content_type [String] the upload file content type after cache
+shared_examples 'upload with content type' do |content_type|
+ let(:fixture_file) { fixture_file_upload(path, content_type) }
+
+ it 'will not change upload file content type' do
+ uploader.cache!(fixture_file)
+ expect(uploader.file.content_type).to eq(content_type)
+ end
+end
diff --git a/spec/uploaders/content_type_whitelist_spec.rb b/spec/uploaders/content_type_whitelist_spec.rb
index be519ead1c8..4689f83759d 100644
--- a/spec/uploaders/content_type_whitelist_spec.rb
+++ b/spec/uploaders/content_type_whitelist_spec.rb
@@ -18,6 +18,7 @@ describe ContentTypeWhitelist do
let(:path) { File.join('spec', 'fixtures', 'rails_sample.jpg') }
it_behaves_like 'accepted carrierwave upload'
+ it_behaves_like 'upload with content type', 'image/jpeg'
end
context 'upload non-whitelisted file content type' do
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index 60b5a6697b1..a03cf3b9dea 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -97,5 +97,12 @@ describe JobArtifactUploader do
it_behaves_like "migrates", to_store: described_class::Store::REMOTE
it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+
+ # CI job artifacts usually are shown as text/plain, but they contain
+ # escape characters so MIME detectors usually fail to determine what
+ # the Content-Type is.
+ it 'does not set Content-Type' do
+ expect(uploader.file.content_type).to be_blank
+ end
end
end