diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-28 15:06:57 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-28 15:06:57 +0000 |
commit | 7cdd70dcec27402e89e65451b4b1feb75b5eb267 (patch) | |
tree | 1691c8e1afd469fa426ecf5bc127de8df16d4855 /spec | |
parent | 79348faced5e7e62103ad27f6a6594dfdca463e2 (diff) | |
download | gitlab-ce-7cdd70dcec27402e89e65451b4b1feb75b5eb267.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
35 files changed, 941 insertions, 443 deletions
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index c8a697cfed4..d4fab48b426 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -43,6 +43,7 @@ describe 'Database schema' do geo_nodes: %w[oauth_application_id], geo_repository_deleted_events: %w[project_id], geo_upload_deleted_events: %w[upload_id model_id], + import_failures: %w[project_id], identities: %w[user_id], issues: %w[last_edited_by_id state_id], jira_tracker_data: %w[jira_issue_transition_id], diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index bcb762664f7..8f83cb77709 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -60,10 +60,20 @@ describe SnippetsFinder do end context 'filter by author' do - it 'returns all public and internal snippets' do - snippets = described_class.new(create(:user), author: user).execute + context 'when the author is a User object' do + it 'returns all public and internal snippets' do + snippets = described_class.new(create(:user), author: user).execute - expect(snippets).to contain_exactly(internal_personal_snippet, public_personal_snippet) + expect(snippets).to contain_exactly(internal_personal_snippet, public_personal_snippet) + end + end + + context 'when the author is the User id' do + it 'returns all public and internal snippets' do + snippets = described_class.new(create(:user), author: user.id).execute + + expect(snippets).to contain_exactly(internal_personal_snippet, public_personal_snippet) + end end it 'returns internal snippets' do @@ -101,13 +111,33 @@ describe SnippetsFinder do expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet) end + + context 'when author is not valid' do + it 'returns quickly' do + finder = described_class.new(admin, author: 1234) + + expect(finder).not_to receive(:init_collection) + expect(Snippet).to receive(:none).and_call_original + expect(finder.execute).to be_empty + end + end end - context 'project snippets' do - it 'returns public personal and project snippets for unauthorized user' do - snippets = described_class.new(nil, project: project).execute + context 'filter by project' do + context 'when project is a Project object' do + it 'returns public personal and project snippets for unauthorized user' do + snippets = described_class.new(nil, project: project).execute - expect(snippets).to contain_exactly(public_project_snippet) + expect(snippets).to contain_exactly(public_project_snippet) + end + end + + context 'when project is a Project id' do + it 'returns public personal and project snippets for unauthorized user' do + snippets = described_class.new(nil, project: project.id).execute + + expect(snippets).to contain_exactly(public_project_snippet) + end end it 'returns public and internal snippets for non project members' do @@ -175,6 +205,49 @@ describe SnippetsFinder do ) end end + + context 'when project is not valid' do + it 'returns quickly' do + finder = described_class.new(admin, project: 1234) + + expect(finder).not_to receive(:init_collection) + expect(Snippet).to receive(:none).and_call_original + expect(finder.execute).to be_empty + end + end + end + + context 'filter by snippet type' do + context 'when filtering by only_personal snippet' do + it 'returns only personal snippet' do + snippets = described_class.new(admin, only_personal: true).execute + + expect(snippets).to contain_exactly(private_personal_snippet, + internal_personal_snippet, + public_personal_snippet) + end + end + + context 'when filtering by only_project snippet' do + it 'returns only project snippet' do + snippets = described_class.new(admin, only_project: true).execute + + expect(snippets).to contain_exactly(private_project_snippet, + internal_project_snippet, + public_project_snippet) + end + end + end + + context 'filtering by ids' do + it 'returns only personal snippet' do + snippets = described_class.new( + admin, ids: [private_personal_snippet.id, + internal_personal_snippet.id] + ).execute + + expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet) + end end context 'explore snippets' do diff --git a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json new file mode 100644 index 00000000000..52b5649ae59 --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json @@ -0,0 +1,38 @@ +{ + "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "import_type": "gitlab_project", + "creator_id": 123, + "visibility_level": 10, + "archived": false, + "milestones": [ + { + "id": 1, + "title": null, + "project_id": 8, + "description": 123, + "due_date": null, + "created_at": "NOT A DATE", + "updated_at": "NOT A DATE", + "state": "active", + "iid": 1, + "group_id": null + }, + { + "id": 42, + "title": "A valid milestone", + "project_id": 8, + "description": "Project-level milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": null + } + ], + "labels": [], + "issues": [], + "services": [], + "snippets": [], + "hooks": [] +} diff --git a/spec/frontend/monitoring/charts/time_series_spec.js b/spec/frontend/monitoring/charts/time_series_spec.js index c561c5edf3c..6fa2d15ccbf 100644 --- a/spec/frontend/monitoring/charts/time_series_spec.js +++ b/spec/frontend/monitoring/charts/time_series_spec.js @@ -48,7 +48,7 @@ describe('Time series component', () => { // Mock data contains 2 panels, pick the first one store.commit(`monitoringDashboard/${types.SET_QUERY_RESULT}`, mockedQueryResultPayload); - [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[0].metrics; + [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[0].panels; makeTimeSeriesChart = (graphData, type) => shallowMount(TimeSeries, { @@ -235,7 +235,7 @@ describe('Time series component', () => { }); it('utilizes all data points', () => { - const { values } = mockGraphData.queries[0].result[0]; + const { values } = mockGraphData.metrics[0].result[0]; expect(chartData.length).toBe(1); expect(seriesData().data.length).toBe(values.length); diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js index 6707d0b1fe8..38aef6e6052 100644 --- a/spec/frontend/monitoring/components/charts/anomaly_spec.js +++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js @@ -17,8 +17,8 @@ const mockProjectPath = `${TEST_HOST}${mockProjectDir}`; jest.mock('~/lib/utils/icon_utils'); // mock getSvgIconPathContent const makeAnomalyGraphData = (datasetName, template = anomalyMockGraphData) => { - const queries = anomalyMockResultValues[datasetName].map((values, index) => ({ - ...template.queries[index], + const metrics = anomalyMockResultValues[datasetName].map((values, index) => ({ + ...template.metrics[index], result: [ { metrics: {}, @@ -26,7 +26,7 @@ const makeAnomalyGraphData = (datasetName, template = anomalyMockGraphData) => { }, ], })); - return { ...template, queries }; + return { ...template, metrics }; }; describe('Anomaly chart component', () => { @@ -67,19 +67,19 @@ describe('Anomaly chart component', () => { describe('graph-data', () => { it('receives a single "metric" series', () => { const { graphData } = getTimeSeriesProps(); - expect(graphData.queries.length).toBe(1); + expect(graphData.metrics.length).toBe(1); }); it('receives "metric" with all data', () => { const { graphData } = getTimeSeriesProps(); - const query = graphData.queries[0]; - const expectedQuery = makeAnomalyGraphData(dataSetName).queries[0]; + const query = graphData.metrics[0]; + const expectedQuery = makeAnomalyGraphData(dataSetName).metrics[0]; expect(query).toEqual(expectedQuery); }); it('receives the "metric" results', () => { const { graphData } = getTimeSeriesProps(); - const { result } = graphData.queries[0]; + const { result } = graphData.metrics[0]; const { values } = result[0]; const [metricDataset] = dataSet; expect(values).toEqual(expect.any(Array)); @@ -266,12 +266,12 @@ describe('Anomaly chart component', () => { describe('graph-data', () => { it('receives a single "metric" series', () => { const { graphData } = getTimeSeriesProps(); - expect(graphData.queries.length).toBe(1); + expect(graphData.metrics.length).toBe(1); }); it('receives "metric" results and applies the offset to them', () => { const { graphData } = getTimeSeriesProps(); - const { result } = graphData.queries[0]; + const { result } = graphData.metrics[0]; const { values } = result[0]; const [metricDataset] = dataSet; expect(values).toEqual(expect.any(Array)); diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js index 3e22b0858e6..c5219f6130e 100644 --- a/spec/frontend/monitoring/embed/embed_spec.js +++ b/spec/frontend/monitoring/embed/embed_spec.js @@ -62,7 +62,7 @@ describe('Embed', () => { describe('metrics are available', () => { beforeEach(() => { store.state.monitoringDashboard.dashboard.panel_groups = groups; - store.state.monitoringDashboard.dashboard.panel_groups[0].metrics = metricsData; + store.state.monitoringDashboard.dashboard.panel_groups[0].panels = metricsData; store.state.monitoringDashboard.metricsWithData = metricsWithData; mountComponent(); diff --git a/spec/frontend/monitoring/embed/mock_data.js b/spec/frontend/monitoring/embed/mock_data.js index 1685021fd4b..8941c183807 100644 --- a/spec/frontend/monitoring/embed/mock_data.js +++ b/spec/frontend/monitoring/embed/mock_data.js @@ -42,38 +42,34 @@ export const metrics = [ }, ]; -const queries = [ +const result = [ { - result: [ - { - values: [ - ['Mon', 1220], - ['Tue', 932], - ['Wed', 901], - ['Thu', 934], - ['Fri', 1290], - ['Sat', 1330], - ['Sun', 1320], - ], - }, + values: [ + ['Mon', 1220], + ['Tue', 932], + ['Wed', 901], + ['Thu', 934], + ['Fri', 1290], + ['Sat', 1330], + ['Sun', 1320], ], }, ]; export const metricsData = [ { - queries, metrics: [ { metric_id: 15, + result, }, ], }, { - queries, metrics: [ { metric_id: 16, + result, }, ], }, diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index c42366ab484..758e86235be 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -110,9 +110,6 @@ export const anomalyMockGraphData = { type: 'anomaly-chart', weight: 3, metrics: [ - // Not used - ], - queries: [ { metricId: '90', id: 'metric', diff --git a/spec/frontend/monitoring/panel_type_spec.js b/spec/frontend/monitoring/panel_type_spec.js index 54a63e7f61f..b30ad747a12 100644 --- a/spec/frontend/monitoring/panel_type_spec.js +++ b/spec/frontend/monitoring/panel_type_spec.js @@ -33,7 +33,7 @@ describe('Panel Type component', () => { let glEmptyChart; // Deep clone object before modifying const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange)); - graphDataNoResult.queries[0].result = []; + graphDataNoResult.metrics[0].result = []; beforeEach(() => { panelType = shallowMount(PanelType, { @@ -143,7 +143,7 @@ describe('Panel Type component', () => { describe('csvText', () => { it('converts metrics data from json to csv', () => { const header = `timestamp,${graphDataPrometheusQueryRange.y_label}`; - const data = graphDataPrometheusQueryRange.queries[0].result[0].values; + const data = graphDataPrometheusQueryRange.metrics[0].result[0].values; const firstRow = `${data[0][0]},${data[0][1]}`; const secondRow = `${data[1][0]},${data[1][1]}`; diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index fdad290a8d6..42031e01878 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -27,7 +27,7 @@ describe('Monitoring mutations', () => { it('normalizes values', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); const expectedLabel = 'Pod average'; - const { label, query_range } = stateCopy.dashboard.panel_groups[0].metrics[0].metrics[0]; + const { label, query_range } = stateCopy.dashboard.panel_groups[0].panels[0].metrics[0]; expect(label).toEqual(expectedLabel); expect(query_range.length).toBeGreaterThan(0); }); @@ -39,23 +39,12 @@ describe('Monitoring mutations', () => { expect(stateCopy.dashboard.panel_groups[0].panels[0].metrics.length).toEqual(1); expect(stateCopy.dashboard.panel_groups[0].panels[1].metrics.length).toEqual(1); }); - it('assigns queries a metric id', () => { + it('assigns metrics a metric id', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); - expect(stateCopy.dashboard.panel_groups[0].metrics[0].queries[0].metricId).toEqual( + expect(stateCopy.dashboard.panel_groups[0].panels[0].metrics[0].metricId).toEqual( '17_system_metrics_kubernetes_container_memory_average', ); }); - describe('dashboard endpoint', () => { - const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups; - it('aliases group panels to metrics for backwards compatibility', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups); - expect(stateCopy.dashboard.panel_groups[0].metrics[0]).toBeDefined(); - }); - it('aliases panel metrics to queries for backwards compatibility', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups); - expect(stateCopy.dashboard.panel_groups[0].metrics[0].queries).toBeDefined(); - }); - }); }); describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => { diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index 98388ac19f8..d562aaaefe9 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -1,44 +1,4 @@ -import { groupQueriesByChartInfo, normalizeMetric, uniqMetricsId } from '~/monitoring/stores/utils'; - -describe('groupQueriesByChartInfo', () => { - let input; - let output; - - it('groups metrics with the same chart title and y_axis label', () => { - input = [ - { title: 'title', y_label: 'MB', queries: [{}] }, - { title: 'title', y_label: 'MB', queries: [{}] }, - { title: 'new title', y_label: 'MB', queries: [{}] }, - ]; - - output = [ - { - title: 'title', - y_label: 'MB', - queries: [{ metricId: null }, { metricId: null }], - }, - { title: 'new title', y_label: 'MB', queries: [{ metricId: null }] }, - ]; - - expect(groupQueriesByChartInfo(input)).toEqual(output); - }); - - // Functionality associated with the /additional_metrics endpoint - it("associates a chart's stringified metric_id with the metric", () => { - input = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{}] }]; - output = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{ metricId: '3' }] }]; - - expect(groupQueriesByChartInfo(input)).toEqual(output); - }); - - // Functionality associated with the /metrics_dashboard endpoint - it('aliases a stringified metrics_id on the metric to the metricId key', () => { - input = [{ title: 'new title', y_label: 'MB', queries: [{ metric_id: 3 }] }]; - output = [{ title: 'new title', y_label: 'MB', queries: [{ metricId: '3', metric_id: 3 }] }]; - - expect(groupQueriesByChartInfo(input)).toEqual(output); - }); -}); +import { normalizeMetric, uniqMetricsId } from '~/monitoring/stores/utils'; describe('normalizeMetric', () => { [ @@ -54,7 +14,7 @@ describe('normalizeMetric', () => { }, ].forEach(({ args, expected }) => { it(`normalizes metric to "${expected}" with args=${JSON.stringify(args)}`, () => { - expect(normalizeMetric(...args)).toEqual({ metric_id: expected }); + expect(normalizeMetric(...args)).toEqual({ metric_id: expected, metricId: expected }); }); }); }); diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js new file mode 100644 index 00000000000..4fb6924daba --- /dev/null +++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js @@ -0,0 +1,60 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +describe('clipboard button', () => { + let wrapper; + + const createWrapper = propsData => { + wrapper = shallowMount(ClipboardButton, { + propsData, + sync: false, + attachToDocument: true, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('without gfm', () => { + beforeEach(() => { + createWrapper({ + text: 'copy me', + title: 'Copy this value', + cssClass: 'btn-danger', + }); + }); + + it('renders a button for clipboard', () => { + expect(wrapper.find(GlButton).exists()).toBe(true); + expect(wrapper.attributes('data-clipboard-text')).toBe('copy me'); + expect(wrapper.find(Icon).props('name')).toBe('duplicate'); + }); + + it('should have a tooltip with default values', () => { + expect(wrapper.attributes('data-original-title')).toBe('Copy this value'); + }); + + it('should render provided classname', () => { + expect(wrapper.classes()).toContain('btn-danger'); + }); + }); + + describe('with gfm', () => { + it('sets data-clipboard-text with gfm', () => { + createWrapper({ + text: 'copy me', + gfm: '`path/to/file`', + title: 'Copy this value', + cssClass: 'btn-danger', + }); + + expect(wrapper.attributes('data-clipboard-text')).toBe( + '{"text":"copy me","gfm":"`path/to/file`"}', + ); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js new file mode 100644 index 00000000000..bdd18110629 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -0,0 +1,113 @@ +import _ from 'underscore'; +import { trimText } from 'helpers/text_helper'; +import { shallowMount } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import { TEST_HOST } from 'spec/test_constants'; + +describe('User Avatar Link Component', () => { + let wrapper; + + const defaultProps = { + linkHref: `${TEST_HOST}/myavatarurl.com`, + imgSize: 99, + imgSrc: `${TEST_HOST}/myavatarurl.com`, + imgAlt: 'mydisplayname', + imgCssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', + username: 'username', + }; + + const createWrapper = props => { + wrapper = shallowMount(UserAvatarLink, { + propsData: { + ...defaultProps, + ...props, + }, + sync: false, + attachToDocument: true, + }); + }; + + beforeEach(() => { + createWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should return a defined Vue component', () => { + expect(wrapper.isVueInstance()).toBe(true); + }); + + it('should have user-avatar-image registered as child component', () => { + expect(wrapper.vm.$options.components.userAvatarImage).toBeDefined(); + }); + + it('user-avatar-link should have user-avatar-image as child component', () => { + expect(wrapper.find(UserAvatarImage).exists()).toBe(true); + }); + + it('should render GlLink as a child element', () => { + const link = wrapper.find(GlLink); + + expect(link.exists()).toBe(true); + expect(link.attributes('href')).toBe(defaultProps.linkHref); + }); + + it('should return necessary props as defined', () => { + _.each(defaultProps, (val, key) => { + expect(wrapper.vm[key]).toBeDefined(); + }); + }); + + describe('no username', () => { + beforeEach(() => { + createWrapper({ + username: '', + }); + }); + + it('should only render image tag in link', () => { + const childElements = wrapper.vm.$el.childNodes; + + expect(wrapper.find('img')).not.toBe('null'); + + // Vue will render the hidden component as <!----> + expect(childElements[1].tagName).toBeUndefined(); + }); + + it('should render avatar image tooltip', () => { + expect(wrapper.vm.shouldShowUsername).toBe(false); + expect(wrapper.vm.avatarTooltipText).toEqual(defaultProps.tooltipText); + }); + }); + + describe('username', () => { + it('should not render avatar image tooltip', () => { + expect(wrapper.find('.js-user-avatar-image-toolip').exists()).toBe(false); + }); + + it('should render username prop in <span>', () => { + expect(trimText(wrapper.find('.js-user-avatar-link-username').text())).toEqual( + defaultProps.username, + ); + }); + + it('should render text tooltip for <span>', () => { + expect( + wrapper.find('.js-user-avatar-link-username').attributes('data-original-title'), + ).toEqual(defaultProps.tooltipText); + }); + + it('should render text tooltip placement for <span>', () => { + expect(wrapper.find('.js-user-avatar-link-username').attributes('tooltip-placement')).toBe( + defaultProps.tooltipPlacement, + ); + }); + }); +}); diff --git a/spec/helpers/award_emoji_helper_spec.rb b/spec/helpers/award_emoji_helper_spec.rb index 2ee27bc5427..2ad6b68a34c 100644 --- a/spec/helpers/award_emoji_helper_spec.rb +++ b/spec/helpers/award_emoji_helper_spec.rb @@ -4,59 +4,69 @@ require 'spec_helper' describe AwardEmojiHelper do describe '.toggle_award_url' do + subject { helper.toggle_award_url(awardable) } + context 'note on personal snippet' do - let(:note) { create(:note_on_personal_snippet) } + let(:snippet) { create(:personal_snippet) } + let(:note) { create(:note_on_personal_snippet, noteable: snippet) } + let(:awardable) { note } + + subject { helper.toggle_award_url(note) } it 'returns correct url' do expected_url = "/snippets/#{note.noteable.id}/notes/#{note.id}/toggle_award_emoji" - expect(helper.toggle_award_url(note)).to eq(expected_url) + expect(subject).to eq(expected_url) end end context 'note on project item' do let(:note) { create(:note_on_project_snippet) } + let(:awardable) { note } it 'returns correct url' do @project = note.noteable.project expected_url = "/#{@project.namespace.path}/#{@project.path}/notes/#{note.id}/toggle_award_emoji" - expect(helper.toggle_award_url(note)).to eq(expected_url) + expect(subject).to eq(expected_url) end end context 'personal snippet' do let(:snippet) { create(:personal_snippet) } + let(:awardable) { snippet } it 'returns correct url' do expected_url = "/snippets/#{snippet.id}/toggle_award_emoji" - expect(helper.toggle_award_url(snippet)).to eq(expected_url) + expect(subject).to eq(expected_url) end end context 'merge request' do let(:merge_request) { create(:merge_request) } + let(:awardable) { merge_request } it 'returns correct url' do @project = merge_request.project expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.iid}/toggle_award_emoji" - expect(helper.toggle_award_url(merge_request)).to eq(expected_url) + expect(subject).to eq(expected_url) end end context 'issue' do let(:issue) { create(:issue) } + let(:awardable) { issue } it 'returns correct url' do @project = issue.project expected_url = "/#{@project.namespace.path}/#{@project.path}/issues/#{issue.iid}/toggle_award_emoji" - expect(helper.toggle_award_url(issue)).to eq(expected_url) + expect(subject).to eq(expected_url) end end end diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb index 38699108b06..9cc1a9dfd5b 100644 --- a/spec/helpers/gitlab_routing_helper_spec.rb +++ b/spec/helpers/gitlab_routing_helper_spec.rb @@ -112,4 +112,98 @@ describe GitlabRoutingHelper do expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/-/milestones/#{milestone.iid}/edit") end end + + context 'snippets' do + let_it_be(:personal_snippet) { create(:personal_snippet) } + let_it_be(:project_snippet) { create(:project_snippet) } + let_it_be(:note) { create(:note_on_personal_snippet, noteable: personal_snippet) } + + describe '#snippet_path' do + it 'returns the personal snippet path' do + expect(snippet_path(personal_snippet)).to eq("/snippets/#{personal_snippet.id}") + end + + it 'returns the project snippet path' do + expect(snippet_path(project_snippet)).to eq("/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}") + end + end + + describe '#snippet_url' do + it 'returns the personal snippet url' do + expect(snippet_url(personal_snippet)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}") + end + + it 'returns the project snippet url' do + expect(snippet_url(project_snippet)).to eq("#{Settings.gitlab['url']}/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}") + end + end + + describe '#raw_snippet_path' do + it 'returns the raw personal snippet path' do + expect(raw_snippet_path(personal_snippet)).to eq("/snippets/#{personal_snippet.id}/raw") + end + + it 'returns the raw project snippet path' do + expect(raw_snippet_path(project_snippet)).to eq("/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}/raw") + end + end + + describe '#raw_snippet_url' do + it 'returns the raw personal snippet url' do + expect(raw_snippet_url(personal_snippet)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/raw") + end + + it 'returns the raw project snippet url' do + expect(raw_snippet_url(project_snippet)).to eq("#{Settings.gitlab['url']}/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}/raw") + end + end + + describe '#snippet_notes_path' do + it 'returns the notes path for the personal snippet' do + expect(snippet_notes_path(personal_snippet)).to eq("/snippets/#{personal_snippet.id}/notes") + end + end + + describe '#snippet_notes_url' do + it 'returns the notes url for the personal snippet' do + expect(snippet_notes_url(personal_snippet)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/notes") + end + end + + describe '#snippet_note_path' do + it 'returns the note path for the personal snippet' do + expect(snippet_note_path(personal_snippet, note)).to eq("/snippets/#{personal_snippet.id}/notes/#{note.id}") + end + end + + describe '#snippet_note_url' do + it 'returns the note url for the personal snippet' do + expect(snippet_note_url(personal_snippet, note)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/notes/#{note.id}") + end + end + + describe '#toggle_award_emoji_snippet_note_path' do + it 'returns the note award emoji path for the personal snippet' do + expect(toggle_award_emoji_snippet_note_path(personal_snippet, note)).to eq("/snippets/#{personal_snippet.id}/notes/#{note.id}/toggle_award_emoji") + end + end + + describe '#toggle_award_emoji_snippet_note_url' do + it 'returns the note award emoji url for the personal snippet' do + expect(toggle_award_emoji_snippet_note_url(personal_snippet, note)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/notes/#{note.id}/toggle_award_emoji") + end + end + + describe '#toggle_award_emoji_snippet_path' do + it 'returns the award emoji path for the personal snippet' do + expect(toggle_award_emoji_snippet_path(personal_snippet)).to eq("/snippets/#{personal_snippet.id}/toggle_award_emoji") + end + end + + describe '#toggle_award_emoji_snippet_url' do + it 'returns the award url for the personal snippet' do + expect(toggle_award_emoji_snippet_url(personal_snippet)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/toggle_award_emoji") + end + end + end end diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb index d88e151a11c..2a24950502b 100644 --- a/spec/helpers/snippets_helper_spec.rb +++ b/spec/helpers/snippets_helper_spec.rb @@ -9,107 +9,18 @@ describe SnippetsHelper do let_it_be(:public_personal_snippet) { create(:personal_snippet, :public) } let_it_be(:public_project_snippet) { create(:project_snippet, :public) } - describe '#reliable_snippet_path' do - subject { reliable_snippet_path(snippet) } - - context 'personal snippets' do - let(:snippet) { public_personal_snippet } - - context 'public' do - it 'returns a full path' do - expect(subject).to eq("/snippets/#{snippet.id}") - end - end - end - - context 'project snippets' do - let(:snippet) { public_project_snippet } - - it 'returns a full path' do - expect(subject).to eq("/#{snippet.project.full_path}/snippets/#{snippet.id}") - end - end - end - - describe '#reliable_snippet_url' do - subject { reliable_snippet_url(snippet) } - - context 'personal snippets' do - let(:snippet) { public_personal_snippet } - - context 'public' do - it 'returns a full url' do - expect(subject).to eq("http://test.host/snippets/#{snippet.id}") - end - end - end - - context 'project snippets' do - let(:snippet) { public_project_snippet } - - it 'returns a full url' do - expect(subject).to eq("http://test.host/#{snippet.project.full_path}/snippets/#{snippet.id}") - end - end - end - - describe '#reliable_raw_snippet_path' do - subject { reliable_raw_snippet_path(snippet) } - - context 'personal snippets' do - let(:snippet) { public_personal_snippet } - - context 'public' do - it 'returns a full path' do - expect(subject).to eq("/snippets/#{snippet.id}/raw") - end - end - end - - context 'project snippets' do - let(:snippet) { public_project_snippet } - - it 'returns a full path' do - expect(subject).to eq("/#{snippet.project.full_path}/snippets/#{snippet.id}/raw") - end - end - end - - describe '#reliable_raw_snippet_url' do - subject { reliable_raw_snippet_url(snippet) } - - context 'personal snippets' do - let(:snippet) { public_personal_snippet } - - context 'public' do - it 'returns a full url' do - expect(subject).to eq("http://test.host/snippets/#{snippet.id}/raw") - end - end - end - - context 'project snippets' do - let(:snippet) { public_project_snippet } - - it 'returns a full url' do - expect(subject).to eq("http://test.host/#{snippet.project.full_path}/snippets/#{snippet.id}/raw") - end - end - end - describe '#embedded_raw_snippet_button' do subject { embedded_raw_snippet_button.to_s } it 'returns view raw button of embedded snippets for personal snippets' do @snippet = create(:personal_snippet, :public) - - expect(subject).to eq(download_link("http://test.host/snippets/#{@snippet.id}/raw")) + expect(subject).to eq(download_link("#{Settings.gitlab['url']}/snippets/#{@snippet.id}/raw")) end it 'returns view raw button of embedded snippets for project snippets' do @snippet = create(:project_snippet, :public) - expect(subject).to eq(download_link("http://test.host/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw")) + expect(subject).to eq(download_link("#{Settings.gitlab['url']}/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw")) end def download_link(url) @@ -123,13 +34,13 @@ describe SnippetsHelper do it 'returns download button of embedded snippets for personal snippets' do @snippet = create(:personal_snippet, :public) - expect(subject).to eq(download_link("http://test.host/snippets/#{@snippet.id}/raw")) + expect(subject).to eq(download_link("#{Settings.gitlab['url']}/snippets/#{@snippet.id}/raw")) end it 'returns download button of embedded snippets for project snippets' do @snippet = create(:project_snippet, :public) - expect(subject).to eq(download_link("http://test.host/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw")) + expect(subject).to eq(download_link("#{Settings.gitlab['url']}/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw")) end def download_link(url) @@ -145,7 +56,7 @@ describe SnippetsHelper do context 'public' do it 'returns a script tag with the snippet full url' do - expect(subject).to eq(script_embed("http://test.host/snippets/#{snippet.id}")) + expect(subject).to eq(script_embed("#{Settings.gitlab['url']}/snippets/#{snippet.id}")) end end end @@ -154,7 +65,7 @@ describe SnippetsHelper do let(:snippet) { public_project_snippet } it 'returns a script tag with the snippet full url' do - expect(subject).to eq(script_embed("http://test.host/#{snippet.project.path_with_namespace}/snippets/#{snippet.id}")) + expect(subject).to eq(script_embed("#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{snippet.id}")) end end diff --git a/spec/javascripts/boards/board_list_common_spec.js b/spec/javascripts/boards/board_list_common_spec.js index ada7589b795..a92d885790d 100644 --- a/spec/javascripts/boards/board_list_common_spec.js +++ b/spec/javascripts/boards/board_list_common_spec.js @@ -9,7 +9,7 @@ import BoardList from '~/boards/components/board_list.vue'; import '~/boards/models/issue'; import '~/boards/models/list'; -import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; +import { listObj, boardsMockInterceptor } from './mock_data'; import store from '~/boards/stores'; import boardsStore from '~/boards/stores/boards_store'; @@ -26,7 +26,6 @@ export default function createComponent({ document.body.appendChild(el); const mock = new MockAdapter(axios); mock.onAny().reply(boardsMockInterceptor); - gl.boardService = mockBoardService(); boardsStore.create(); const BoardListComp = Vue.extend(BoardList); diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 76675a78db2..8e4093cc25c 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -7,7 +7,7 @@ import boardNewIssue from '~/boards/components/board_new_issue.vue'; import boardsStore from '~/boards/stores/boards_store'; import '~/boards/models/list'; -import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; +import { listObj, boardsMockInterceptor } from './mock_data'; describe('Issue boards new issue form', () => { let vm; @@ -36,7 +36,6 @@ describe('Issue boards new issue form', () => { mock = new MockAdapter(axios); mock.onAny().reply(boardsMockInterceptor); - gl.boardService = mockBoardService(); boardsStore.create(); list = new List(listObj); diff --git a/spec/javascripts/monitoring/charts/column_spec.js b/spec/javascripts/monitoring/charts/column_spec.js index 27b3d435f08..9676617e8e1 100644 --- a/spec/javascripts/monitoring/charts/column_spec.js +++ b/spec/javascripts/monitoring/charts/column_spec.js @@ -11,7 +11,7 @@ describe('Column component', () => { columnChart = shallowMount(localVue.extend(ColumnChart), { propsData: { graphData: { - queries: [ + metrics: [ { x_label: 'Time', y_label: 'Usage', diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index f9cc839bde6..f59c4ee4264 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -105,22 +105,11 @@ export const graphDataPrometheusQuery = { metrics: [ { id: 'metric_a1', - metric_id: 2, + metricId: '2', query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024', unit: 'MB', label: 'Total Consumption', - prometheus_endpoint_path: - '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', - }, - ], - queries: [ - { - metricId: null, - id: 'metric_a1', metric_id: 2, - query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024', - unit: 'MB', - label: 'Total Consumption', prometheus_endpoint_path: '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', result: [ @@ -140,24 +129,12 @@ export const graphDataPrometheusQueryRange = { metrics: [ { id: 'metric_a1', - metric_id: 2, + metricId: '2', query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', unit: 'MB', label: 'Total Consumption', - prometheus_endpoint_path: - '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', - }, - ], - queries: [ - { - metricId: '10', - id: 'metric_a1', metric_id: 2, - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', - unit: 'MB', - label: 'Total Consumption', prometheus_endpoint_path: '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', result: [ @@ -176,8 +153,7 @@ export const graphDataPrometheusQueryRangeMultiTrack = { weight: 3, x_label: 'Status Code', y_label: 'Time', - metrics: [], - queries: [ + metrics: [ { metricId: '1', id: 'response_metrics_nginx_ingress_throughput_status_code', diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js index 202b4ec8f2e..3459b44c7ec 100644 --- a/spec/javascripts/monitoring/utils_spec.js +++ b/spec/javascripts/monitoring/utils_spec.js @@ -314,32 +314,32 @@ describe('isDateTimePickerInputValid', () => { }); describe('graphDataValidatorForAnomalyValues', () => { - let oneQuery; - let threeQueries; - let fourQueries; + let oneMetric; + let threeMetrics; + let fourMetrics; beforeEach(() => { - oneQuery = graphDataPrometheusQuery; - threeQueries = anomalyMockGraphData; + oneMetric = graphDataPrometheusQuery; + threeMetrics = anomalyMockGraphData; - const queries = [...threeQueries.queries]; - queries.push(threeQueries.queries[0]); - fourQueries = { + const metrics = [...threeMetrics.metrics]; + metrics.push(threeMetrics.metrics[0]); + fourMetrics = { ...anomalyMockGraphData, - queries, + metrics, }; }); /* - * Anomaly charts can accept results for exactly 3 queries, + * Anomaly charts can accept results for exactly 3 metrics, */ it('validates passes with the right query format', () => { - expect(graphDataValidatorForAnomalyValues(threeQueries)).toBe(true); + expect(graphDataValidatorForAnomalyValues(threeMetrics)).toBe(true); }); it('validation fails for wrong format, 1 metric', () => { - expect(graphDataValidatorForAnomalyValues(oneQuery)).toBe(false); + expect(graphDataValidatorForAnomalyValues(oneMetric)).toBe(false); }); it('validation fails for wrong format, more than 3 metrics', () => { - expect(graphDataValidatorForAnomalyValues(fourQueries)).toBe(false); + expect(graphDataValidatorForAnomalyValues(fourMetrics)).toBe(false); }); }); diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js deleted file mode 100644 index 29a76574b89..00000000000 --- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import Vue from 'vue'; -import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('clipboard button', () => { - const Component = Vue.extend(clipboardButton); - let vm; - - afterEach(() => { - vm.$destroy(); - }); - - describe('without gfm', () => { - beforeEach(() => { - vm = mountComponent(Component, { - text: 'copy me', - title: 'Copy this value', - cssClass: 'btn-danger', - }); - }); - - it('renders a button for clipboard', () => { - expect(vm.$el.tagName).toEqual('BUTTON'); - expect(vm.$el.getAttribute('data-clipboard-text')).toEqual('copy me'); - expect(vm.$el).toHaveSpriteIcon('duplicate'); - }); - - it('should have a tooltip with default values', () => { - expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value'); - }); - - it('should render provided classname', () => { - expect(vm.$el.classList).toContain('btn-danger'); - }); - }); - - describe('with gfm', () => { - it('sets data-clipboard-text with gfm', () => { - vm = mountComponent(Component, { - text: 'copy me', - gfm: '`path/to/file`', - title: 'Copy this value', - cssClass: 'btn-danger', - }); - - expect(vm.$el.getAttribute('data-clipboard-text')).toEqual( - '{"text":"copy me","gfm":"`path/to/file`"}', - ); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js deleted file mode 100644 index 80aa75847ae..00000000000 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import _ from 'underscore'; -import Vue from 'vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import { TEST_HOST } from 'spec/test_constants'; - -describe('User Avatar Link Component', function() { - beforeEach(function() { - this.propsData = { - linkHref: `${TEST_HOST}/myavatarurl.com`, - imgSize: 99, - imgSrc: `${TEST_HOST}/myavatarurl.com`, - imgAlt: 'mydisplayname', - imgCssClasses: 'myextraavatarclass', - tooltipText: 'tooltip text', - tooltipPlacement: 'bottom', - username: 'username', - }; - - const UserAvatarLinkComponent = Vue.extend(UserAvatarLink); - - this.userAvatarLink = new UserAvatarLinkComponent({ - propsData: this.propsData, - }).$mount(); - - [this.userAvatarImage] = this.userAvatarLink.$children; - }); - - it('should return a defined Vue component', function() { - expect(this.userAvatarLink).toBeDefined(); - }); - - it('should have user-avatar-image registered as child component', function() { - expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined(); - }); - - it('user-avatar-link should have user-avatar-image as child component', function() { - expect(this.userAvatarImage).toBeDefined(); - }); - - it('should render <a> as a child element', function() { - const link = this.userAvatarLink.$el; - - expect(link.tagName).toBe('A'); - expect(link.href).toBe(this.propsData.linkHref); - }); - - it('renders imgSrc with imgSize as image', function() { - const { imgSrc, imgSize } = this.propsData; - const image = this.userAvatarLink.$el.querySelector('img'); - - expect(image).not.toBeNull(); - expect(image.src).toBe(`${imgSrc}?width=${imgSize}`); - }); - - it('should return necessary props as defined', function() { - _.each(this.propsData, (val, key) => { - expect(this.userAvatarLink[key]).toBeDefined(); - }); - }); - - describe('no username', function() { - beforeEach(function(done) { - this.userAvatarLink.username = ''; - - Vue.nextTick(done); - }); - - it('should only render image tag in link', function() { - const childElements = this.userAvatarLink.$el.childNodes; - - expect(this.userAvatarLink.$el.querySelector('img')).not.toBe('null'); - - // Vue will render the hidden component as <!----> - expect(childElements[1].tagName).toBeUndefined(); - }); - - it('should render avatar image tooltip', function() { - expect(this.userAvatarLink.shouldShowUsername).toBe(false); - expect(this.userAvatarLink.avatarTooltipText).toEqual(this.propsData.tooltipText); - }); - }); - - describe('username', function() { - it('should not render avatar image tooltip', function() { - expect(this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip')).toBeNull(); - }); - - it('should render username prop in <span>', function() { - expect( - this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').innerText.trim(), - ).toEqual(this.propsData.username); - }); - - it('should render text tooltip for <span>', function() { - expect( - this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').dataset - .originalTitle, - ).toEqual(this.propsData.tooltipText); - }); - - it('should render text tooltip placement for <span>', function() { - expect( - this.userAvatarLink.$el - .querySelector('.js-user-avatar-link-username') - .getAttribute('tooltip-placement'), - ).toEqual(this.propsData.tooltipPlacement); - }); - }); -}); diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index 8fcd4eb3c21..e25ce4df4aa 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -12,6 +12,15 @@ describe Gitlab::EtagCaching::Router do expect(result.name).to eq 'issue_notes' end + it 'matches MR notes endpoint' do + result = described_class.match( + '/my-group/and-subgroup/here-comes-the-project/noteable/merge_request/1/notes' + ) + + expect(result).to be_present + expect(result.name).to eq 'merge_request_notes' + end + it 'matches issue title endpoint' do result = described_class.match( '/my-group/my-project/issues/123/realtime_changes' diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 5612b0dc270..64d1a98ae71 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -365,6 +365,7 @@ project: - root_of_fork_network - fork_network_member - fork_network +- fork_network_projects - custom_attributes - lfs_file_locks - project_badges @@ -434,6 +435,7 @@ project: - upstream_project_subscriptions - downstream_project_subscriptions - service_desk_setting +- import_failures award_emoji: - awardable - user diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 2d8a603172d..d0e5ca2dde3 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -362,7 +362,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(restored_project_json).to eq(true) end - it_behaves_like 'restores project correctly', + it_behaves_like 'restores project successfully', issues: 1, labels: 2, label_with_priorities: 'A project label', @@ -375,7 +375,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do create(:ci_build, token: 'abcd') end - it_behaves_like 'restores project correctly', + it_behaves_like 'restores project successfully', issues: 1, labels: 2, label_with_priorities: 'A project label', @@ -452,7 +452,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(restored_project_json).to eq(true) end - it_behaves_like 'restores project correctly', + it_behaves_like 'restores project successfully', issues: 2, labels: 2, label_with_priorities: 'A project label', @@ -633,4 +633,46 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end end + + context 'JSON with invalid records' do + let(:user) { create(:user) } + let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + let(:restored_project_json) { project_tree_restorer.restore } + + context 'when some failures occur' do + context 'because a relation fails to be processed' do + let(:correlation_id) { 'my-correlation-id' } + + before do + setup_import_export_config('with_invalid_records') + + Labkit::Correlation::CorrelationId.use_id(correlation_id) do + expect(restored_project_json).to eq(true) + end + end + + it_behaves_like 'restores project successfully', + issues: 0, + labels: 0, + label_with_priorities: nil, + milestones: 1, + first_issue_labels: 0, + services: 0, + import_failures: 1 + + it 'records the failures in the database' do + import_failure = ImportFailure.last + + expect(import_failure.project_id).to eq(project.id) + expect(import_failure.relation_key).to eq('milestones') + expect(import_failure.relation_index).to be_present + expect(import_failure.exception_class).to eq('ActiveRecord::RecordInvalid') + expect(import_failure.exception_message).to be_present + expect(import_failure.correlation_id_value).to eq('my-correlation-id') + expect(import_failure.created_at).to be_present + end + end + end + end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 08d3c638f9e..0aab02b6c4c 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -59,6 +59,26 @@ describe Gitlab::UrlBuilder do end end + context 'when passing a ProjectSnippet' do + it 'returns a proper URL' do + project_snippet = create(:project_snippet) + + url = described_class.build(project_snippet) + + expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}" + end + end + + context 'when passing a PersonalSnippet' do + it 'returns a proper URL' do + personal_snippet = create(:personal_snippet) + + url = described_class.build(personal_snippet) + + expect(url).to eq "#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}" + end + end + context 'when passing a Note' do context 'on a Commit' do it 'returns a proper URL' do diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 75dc7d8e6d1..16a05af2216 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -95,4 +95,28 @@ describe Gitlab::VisibilityLevel do expect(described_class.valid_level?(described_class::PUBLIC)).to be_truthy end end + + describe '#visibility_level_decreased?' do + let(:project) { create(:project, :internal) } + + context 'when visibility level decreases' do + before do + project.update!(visibility_level: described_class::PRIVATE) + end + + it 'returns true' do + expect(project.visibility_level_decreased?).to be(true) + end + end + + context 'when visibility level does not decrease' do + before do + project.update!(visibility_level: described_class::PUBLIC) + end + + it 'returns false' do + expect(project.visibility_level_decreased?).to be(false) + end + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 79ea29c84a4..f3d270cdf7e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -33,6 +33,21 @@ describe MergeRequest do end end + describe '.from_and_to_forks' do + it 'returns only MRs from and to forks (with no internal MRs)' do + project = create(:project) + fork = fork_project(project) + fork_2 = fork_project(project) + mr_from_fork = create(:merge_request, source_project: fork, target_project: project) + mr_to_fork = create(:merge_request, source_project: project, target_project: fork) + + create(:merge_request, source_project: fork, target_project: fork_2) + create(:merge_request, source_project: project, target_project: project) + + expect(described_class.from_and_to_forks(project)).to contain_exactly(mr_from_fork, mr_to_fork) + end + end + describe 'locking' do using RSpec::Parameterized::TableSyntax diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 74f2fc1bb61..a6d9ecaa7c5 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -1048,20 +1048,20 @@ describe Note do describe 'expiring ETag cache' do let(:note) { build(:note_on_issue) } - def expect_expiration(note) + def expect_expiration(noteable) expect_any_instance_of(Gitlab::EtagCaching::Store) .to receive(:touch) - .with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes") + .with("/#{noteable.project.namespace.to_param}/#{noteable.project.to_param}/noteable/#{noteable.class.name.underscore}/#{noteable.id}/notes") end it "expires cache for note's issue when note is saved" do - expect_expiration(note) + expect_expiration(note.noteable) note.save! end it "expires cache for note's issue when note is destroyed" do - expect_expiration(note) + expect_expiration(note.noteable) note.destroy! end @@ -1076,28 +1076,54 @@ describe Note do end end - describe '#with_notes_filter' do - let!(:comment) { create(:note) } - let!(:system_note) { create(:note, system: true) } + context 'for merge requests' do + let_it_be(:merge_request) { create(:merge_request) } - context 'when notes filter is nil' do - subject { described_class.with_notes_filter(nil) } + context 'when adding a note to the MR' do + let(:note) { build(:note, noteable: merge_request, project: merge_request.project) } - it { is_expected.to include(comment, system_note) } + it 'expires the MR note etag cache' do + expect_expiration(merge_request) + + note.save! + end end - context 'when notes filter is set to all notes' do - subject { described_class.with_notes_filter(UserPreference::NOTES_FILTERS[:all_notes]) } + context 'when adding a note to a commit on the MR' do + let(:note) { build(:note_on_commit, commit_id: merge_request.commits.first.id, project: merge_request.project) } - it { is_expected.to include(comment, system_note) } + it 'expires the MR note etag cache' do + expect_expiration(merge_request) + + note.save! + end end + end + end - context 'when notes filter is set to only comments' do - subject { described_class.with_notes_filter(UserPreference::NOTES_FILTERS[:only_comments]) } + describe '#with_notes_filter' do + let!(:comment) { create(:note) } + let!(:system_note) { create(:note, system: true) } - it { is_expected.to include(comment) } - it { is_expected.not_to include(system_note) } - end + subject { described_class.with_notes_filter(filter) } + + context 'when notes filter is nil' do + let(:filter) { nil } + + it { is_expected.to include(comment, system_note) } + end + + context 'when notes filter is set to all notes' do + let(:filter) { UserPreference::NOTES_FILTERS[:all_notes] } + + it { is_expected.to include(comment, system_note) } + end + + context 'when notes filter is set to only comments' do + let(:filter) { UserPreference::NOTES_FILTERS[:only_comments] } + + it { is_expected.to include(comment) } + it { is_expected.not_to include(system_note) } end end diff --git a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb index f200c636aac..a772b911d8a 100644 --- a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb +++ b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb @@ -175,3 +175,64 @@ describe Metrics::Dashboard::GrafanaMetricEmbedService do end end end + +describe Metrics::Dashboard::GrafanaUidParser do + let_it_be(:grafana_integration) { create(:grafana_integration) } + let_it_be(:project) { grafana_integration.project } + + subject { described_class.new(grafana_url, project).parse } + + context 'with a Grafana-defined uid' do + let(:grafana_url) { grafana_integration.grafana_url + '/d/XDaNK6amz/?panelId=1' } + + it { is_expected.to eq 'XDaNK6amz' } + end + + context 'with a user-defined uid' do + let(:grafana_url) { grafana_integration.grafana_url + '/d/pgbouncer-main/pgbouncer-overview?panelId=1' } + + it { is_expected.to eq 'pgbouncer-main' } + end + + context 'when a uid is not present' do + let(:grafana_url) { grafana_integration.grafana_url } + + it { is_expected.to be nil } + end + + context 'when the url starts with unrelated content' do + let(:grafana_url) { 'js:' + grafana_integration.grafana_url } + + it { is_expected.to be nil } + end +end + +describe Metrics::Dashboard::DatasourceNameParser do + include GrafanaApiHelpers + + let(:grafana_url) { valid_grafana_dashboard_link('https://gitlab.grafana.net') } + let(:grafana_dashboard) { JSON.parse(fixture_file('grafana/dashboard_response.json'), symbolize_names: true) } + + subject { described_class.new(grafana_url, grafana_dashboard).parse } + + it { is_expected.to eq 'GitLab Omnibus' } + + context 'when the panelId is missing from the url' do + let(:grafana_url) { 'https:/gitlab.grafana.net/d/jbdbks/' } + + it { is_expected.to be nil } + end + + context 'when the panel is not present' do + # We're looking for panelId of 8, but only 6 is present + let(:grafana_dashboard) { { dashboard: { panels: [{ id: 6 }] } } } + + it { is_expected.to be nil } + end + + context 'when the dashboard panel does not have a datasource' do + let(:grafana_dashboard) { { dashboard: { panels: [{ id: 8 }] } } } + + it { is_expected.to be nil } + end +end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 642986bb176..d8ba042af35 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -296,9 +296,12 @@ describe Projects::DestroyService do end context 'as the root of a fork network' do - let!(:fork_network) { create(:fork_network, root_project: project) } + let!(:fork_1) { fork_project(project, user) } + let!(:fork_2) { fork_project(project, user) } it 'updates the fork network with the project name' do + fork_network = project.fork_network + destroy_project(project, user) fork_network.reload diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index a1175bf7123..a6bdc69cdca 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' -describe Projects::UnlinkForkService do +describe Projects::UnlinkForkService, :use_clean_rails_memory_store_caching do include ProjectForksHelper subject { described_class.new(forked_project, user) } let(:project) { create(:project, :public) } - let(:forked_project) { fork_project(project, user) } + let!(:forked_project) { fork_project(project, user) } let(:user) { create(:user) } context 'with opened merge request on the source project' do @@ -86,4 +86,169 @@ describe Projects::UnlinkForkService do expect { subject.execute }.not_to raise_error end end + + context 'when given project is a source of forks' do + let!(:forked_project_2) { fork_project(project, user) } + let!(:fork_of_fork) { fork_project(forked_project, user) } + + subject { described_class.new(project, user) } + + context 'with opened merge requests from fork back to root project' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: forked_project) } + let!(:merge_request2) { create(:merge_request, source_project: project, target_project: fork_project(project)) } + let!(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) } + + let(:mr_close_service) { MergeRequests::CloseService.new(project, user) } + + before do + allow(MergeRequests::CloseService).to receive(:new) + .with(project, user) + .and_return(mr_close_service) + end + + it 'closes all pending merge requests' do + expect(mr_close_service).to receive(:execute).with(merge_request) + expect(mr_close_service).to receive(:execute).with(merge_request2) + + subject.execute + end + + it 'does not close merge requests that do not come from the project being unlinked' do + expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork) + + subject.execute + end + end + + it 'removes its link to the fork network and updates direct network members' do + expect(project.fork_network_member).to be_present + expect(project.fork_network).to be_present + expect(project.forked_to_members.count).to eq(2) + expect(forked_project.forked_to_members.count).to eq(1) + expect(fork_of_fork.forked_to_members.count).to eq(0) + + subject.execute + + project.reload + forked_project.reload + fork_of_fork.reload + + expect(project.fork_network_member).to be_nil + expect(project.fork_network).to be_nil + expect(forked_project.fork_network).to have_attributes(root_project_id: nil, + deleted_root_project_name: project.full_name) + expect(project.forked_to_members.count).to eq(0) + expect(forked_project.forked_to_members.count).to eq(1) + expect(fork_of_fork.forked_to_members.count).to eq(0) + end + + it 'refreshes the forks count cache of the given project' do + expect(project.forks_count).to eq(2) + + subject.execute + + expect(project.forks_count).to be_zero + end + + context 'when given project is a fork of an unlinked parent' do + let!(:fork_of_fork) { fork_project(forked_project, user) } + let(:lfs_object) { create(:lfs_object) } + + before do + lfs_object.projects << project + end + + it 'saves lfs objects to the root project' do + # Remove parent from network + described_class.new(forked_project, user).execute + + described_class.new(fork_of_fork, user).execute + + expect(lfs_object.projects).to include(fork_of_fork) + end + end + + context 'and is node with a parent' do + subject { described_class.new(forked_project, user) } + + context 'with opened merge requests from and to given project' do + let!(:mr_from_parent) { create(:merge_request, source_project: project, target_project: forked_project) } + let!(:mr_to_parent) { create(:merge_request, source_project: forked_project, target_project: project) } + let!(:mr_to_child) { create(:merge_request, source_project: forked_project, target_project: fork_of_fork) } + let!(:mr_from_child) { create(:merge_request, source_project: fork_of_fork, target_project: forked_project) } + let!(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) } + + let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) } + + before do + allow(MergeRequests::CloseService).to receive(:new) + .with(forked_project, user) + .and_return(mr_close_service) + end + + it 'close all pending merge requests' do + merge_requests = [mr_from_parent, mr_to_parent, mr_from_child, mr_to_child] + + merge_requests.each do |mr| + expect(mr_close_service).to receive(:execute).with(mr).and_call_original + end + + subject.execute + + merge_requests = MergeRequest.where(id: merge_requests) + + expect(merge_requests).to all(have_attributes(state: 'closed')) + end + + it 'does not close merge requests which do not come from the project being unlinked' do + expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork) + + subject.execute + end + end + + it 'refreshes the forks count cache of the parent and the given project' do + expect(project.forks_count).to eq(2) + expect(forked_project.forks_count).to eq(1) + + subject.execute + + expect(project.forks_count).to eq(1) + expect(forked_project.forks_count).to eq(0) + end + + it 'removes its link to the fork network and updates direct network members' do + expect(project.fork_network).to be_present + expect(forked_project.fork_network).to be_present + expect(fork_of_fork.fork_network).to be_present + + expect(project.forked_to_members.count).to eq(2) + expect(forked_project.forked_to_members.count).to eq(1) + expect(fork_of_fork.forked_to_members.count).to eq(0) + + subject.execute + project.reload + forked_project.reload + fork_of_fork.reload + + expect(project.fork_network).to be_present + expect(forked_project.fork_network).to be_nil + expect(fork_of_fork.fork_network).to be_present + + expect(project.forked_to_members.count).to eq(1) # 1 child is gone + expect(forked_project.forked_to_members.count).to eq(0) + expect(fork_of_fork.forked_to_members.count).to eq(0) + end + end + end + + context 'when given project is not part of a fork network' do + let!(:project_without_forks) { create(:project, :public) } + + subject { described_class.new(project_without_forks, user) } + + it 'does not raise errors' do + expect { subject.execute }.not_to raise_error + end + end end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index c848a5397e1..3092fb7116a 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -16,7 +16,14 @@ describe Projects::UpdateService do let(:admin) { create(:admin) } context 'when changing visibility level' do - context 'when visibility_level is INTERNAL' do + def expect_to_call_unlink_fork_service + service = Projects::UnlinkForkService.new(project, user) + + expect(Projects::UnlinkForkService).to receive(:new).with(project, user).and_return(service) + expect(service).to receive(:execute).and_call_original + end + + context 'when visibility_level changes to INTERNAL' do it 'updates the project to internal' do expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in) @@ -25,9 +32,21 @@ describe Projects::UpdateService do expect(result).to eq({ status: :success }) expect(project).to be_internal end + + context 'and project is PUBLIC' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + it 'unlinks project from fork network' do + expect_to_call_unlink_fork_service + + update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + end end - context 'when visibility_level is PUBLIC' do + context 'when visibility_level changes to PUBLIC' do it 'updates the project to public' do expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in) @@ -36,9 +55,17 @@ describe Projects::UpdateService do expect(result).to eq({ status: :success }) expect(project).to be_public end + + context 'and project is PRIVATE' do + it 'does not unlink project from fork network' do + expect(Projects::UnlinkForkService).not_to receive(:new) + + update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + end end - context 'when visibility_level is PRIVATE' do + context 'when visibility_level changes to PRIVATE' do before do project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) end @@ -52,6 +79,30 @@ describe Projects::UpdateService do expect(result).to eq({ status: :success }) expect(project).to be_private end + + context 'and project is PUBLIC' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + it 'unlinks project from fork network' do + expect_to_call_unlink_fork_service + + update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + end + + context 'and project is INTERNAL' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + it 'unlinks project from fork network' do + expect_to_call_unlink_fork_service + + update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + end end context 'when visibility levels are restricted to PUBLIC only' do @@ -107,28 +158,48 @@ describe Projects::UpdateService do let(:project) { create(:project, :internal) } let(:forked_project) { fork_project(project) } - it 'updates forks visibility level when parent set to more restrictive' do - opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } + context 'and unlink forks feature flag is off' do + before do + stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false) + end + + it 'updates forks visibility level when parent set to more restrictive' do + opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } + + expect(project).to be_internal + expect(forked_project).to be_internal + + expect(update_project(project, admin, opts)).to eq({ status: :success }) + + expect(project).to be_private + expect(forked_project.reload).to be_private + end + + it 'does not update forks visibility level when parent set to less restrictive' do + opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } - expect(project).to be_internal - expect(forked_project).to be_internal + expect(project).to be_internal + expect(forked_project).to be_internal - expect(update_project(project, admin, opts)).to eq({ status: :success }) + expect(update_project(project, admin, opts)).to eq({ status: :success }) - expect(project).to be_private - expect(forked_project.reload).to be_private + expect(project).to be_public + expect(forked_project.reload).to be_internal + end end - it 'does not update forks visibility level when parent set to less restrictive' do - opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } + context 'and unlink forks feature flag is on' do + it 'does not change visibility of forks' do + opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } - expect(project).to be_internal - expect(forked_project).to be_internal + expect(project).to be_internal + expect(forked_project).to be_internal - expect(update_project(project, admin, opts)).to eq({ status: :success }) + expect(update_project(project, admin, opts)).to eq({ status: :success }) - expect(project).to be_public - expect(forked_project.reload).to be_internal + expect(project).to be_private + expect(forked_project.reload).to be_internal + end end end diff --git a/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb index f26a8554055..2fee58a9f30 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb @@ -2,7 +2,7 @@ # Shared examples for ProjectTreeRestorer (shared to allow the testing # of EE-specific features) -RSpec.shared_examples 'restores project correctly' do |**results| +RSpec.shared_examples 'restores project successfully' do |**results| it 'restores the project' do expect(shared.errors).to be_empty expect(restored_project_json).to be_truthy @@ -34,4 +34,8 @@ RSpec.shared_examples 'restores project correctly' do |**results| expect(project.import_type).to be_nil expect(project.creator_id).not_to eq 123 end + + it 'records exact number of import failures' do + expect(project.import_failures.size).to eq(results.fetch(:import_failures, 0)) + end end |