diff options
Diffstat (limited to 'spec')
49 files changed, 2720 insertions, 120 deletions
diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb index 3a6a037ac9a..f6705ba6acc 100644 --- a/spec/controllers/concerns/metrics_dashboard_spec.rb +++ b/spec/controllers/concerns/metrics_dashboard_spec.rb @@ -134,10 +134,10 @@ describe MetricsDashboard do it 'adds starred dashboard information and sorts the list' do all_dashboards = json_response['all_dashboards'].map { |dashboard| dashboard.slice('display_name', 'starred', 'user_starred_path') } expected_response = [ - { "display_name" => "errors.yml", "starred" => true, 'user_starred_path' => nil }, - { "display_name" => "test.yml", "starred" => true, 'user_starred_path' => nil }, + { "display_name" => "Default", "starred" => false, 'user_starred_path' => nil }, { "display_name" => "anomaly.yml", "starred" => false, 'user_starred_path' => nil }, - { "display_name" => "Default", "starred" => false, 'user_starred_path' => nil } + { "display_name" => "errors.yml", "starred" => true, 'user_starred_path' => nil }, + { "display_name" => "test.yml", "starred" => true, 'user_starred_path' => nil } ] expect(all_dashboards).to eql expected_response diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js index 2758014aaa7..5234be0bf07 100644 --- a/spec/frontend/alert_management/components/alert_management_detail_spec.js +++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js @@ -1,4 +1,4 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import AlertDetails from '~/alert_management/components/alert_details.vue'; @@ -11,18 +11,20 @@ describe('AlertDetails', () => { const newIssuePath = 'root/alerts/-/issues/new'; function mountComponent({ - data = { alert: {} }, + data, createIssueFromAlertEnabled = false, loading = false, + mountMethod = shallowMount, + stubs = {}, } = {}) { - wrapper = shallowMount(AlertDetails, { + wrapper = mountMethod(AlertDetails, { propsData: { alertId: 'alertId', projectPath: 'projectPath', newIssuePath, }, data() { - return data; + return { alert: { ...mockAlert }, ...data }; }, provide: { glFeatures: { createIssueFromAlertEnabled }, @@ -36,6 +38,7 @@ describe('AlertDetails', () => { }, }, }, + stubs, }); } @@ -149,5 +152,33 @@ describe('AlertDetails', () => { expect(wrapper.find(GlAlert).exists()).toBe(false); }); }); + + describe('header', () => { + const findHeader = () => wrapper.find('[data-testid="alert-header"]'); + const stubs = { TimeAgoTooltip: '<span>now</span>' }; + + describe('individual header fields', () => { + describe.each` + severity | createdAt | monitoringTool | result + ${'MEDIUM'} | ${'2020-04-17T23:18:14.996Z'} | ${null} | ${'Medium • Reported now'} + ${'INFO'} | ${'2020-04-17T23:18:14.996Z'} | ${'Datadog'} | ${'Info • Reported now by Datadog'} + `( + `When severity=$severity, createdAt=$createdAt, monitoringTool=$monitoringTool`, + ({ severity, createdAt, monitoringTool, result }) => { + beforeEach(() => { + mountComponent({ + data: { alert: { ...mockAlert, severity, createdAt, monitoringTool } }, + mountMethod: mount, + stubs, + }); + }); + + it('header text is shown correctly', () => { + expect(findHeader().text()).toBe(result); + }); + }, + ); + }); + }); }); }); diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js index 23da4df188b..597574bdeed 100644 --- a/spec/frontend/ide/components/new_dropdown/modal_spec.js +++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js @@ -91,22 +91,31 @@ describe('new file modal component', () => { expect(vm.entryName).toBe('test-path'); }); - it('updated name', () => { - vm.name = 'index.js'; + it('does not reset entryName to its old value if empty', () => { + vm.entryName = 'hello'; + vm.entryName = ''; - expect(vm.entryName).toBe('index.js'); + expect(vm.entryName).toBe(''); + }); + }); + + describe('open', () => { + it('sets entryName to path provided if modalType is rename', () => { + vm.open('rename', 'test-path'); + + expect(vm.entryName).toBe('test-path'); }); - it('removes leading/trailing spaces when found in the new name', () => { - vm.entryName = ' index.js '; + it("appends '/' to the path if modalType isn't rename", () => { + vm.open('blob', 'test-path'); - expect(vm.entryName).toBe('index.js'); + expect(vm.entryName).toBe('test-path/'); }); - it('does not remove internal spaces in the file name', () => { - vm.entryName = ' In Praise of Idleness.txt '; + it('leaves entryName blank if no path is provided', () => { + vm.open('blob'); - expect(vm.entryName).toBe('In Praise of Idleness.txt'); + expect(vm.entryName).toBe(''); }); }); }); diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js index 7271beea50a..51c0e192c58 100644 --- a/spec/frontend/monitoring/components/variables_section_spec.js +++ b/spec/frontend/monitoring/components/variables_section_spec.js @@ -15,8 +15,8 @@ describe('Metrics dashboard/variables section component', () => { let store; let wrapper; const sampleVariables = { - label1: 'pod', - label2: 'main', + 'var-label1': 'pod', + 'var-label2': 'main', }; const createShallowWrapper = () => { diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 2fa88dfa87a..a8443c08b00 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -642,7 +642,7 @@ const generateMockTemplatingData = data => { const responseForSimpleTextVariable = { simpleText: { - label: 'simpleText', + label: 'var-simpleText', type: 'text', value: 'Simple text', }, @@ -650,7 +650,7 @@ const responseForSimpleTextVariable = { const responseForAdvTextVariable = { advText: { - label: 'Variable 4', + label: 'var-Variable 4', type: 'text', value: 'default', }, @@ -658,7 +658,7 @@ const responseForAdvTextVariable = { const responseForSimpleCustomVariable = { simpleCustom: { - label: 'simpleCustom', + label: 'var-simpleCustom', options: [ { default: false, @@ -682,7 +682,7 @@ const responseForSimpleCustomVariable = { const responseForAdvancedCustomVariableWithoutOptions = { advCustomWithoutOpts: { - label: 'advCustomWithoutOpts', + label: 'var-advCustomWithoutOpts', options: [], type: 'custom', }, @@ -690,7 +690,7 @@ const responseForAdvancedCustomVariableWithoutOptions = { const responseForAdvancedCustomVariableWithoutLabel = { advCustomWithoutLabel: { - label: 'advCustomWithoutLabel', + label: 'var-advCustomWithoutLabel', options: [ { default: false, @@ -710,7 +710,7 @@ const responseForAdvancedCustomVariableWithoutLabel = { const responseForAdvancedCustomVariable = { ...responseForSimpleCustomVariable, advCustomNormal: { - label: 'Advanced Var', + label: 'var-Advanced Var', options: [ { default: false, diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js index f07ae4c5a1e..e3ee9ffb2bc 100644 --- a/spec/frontend/monitoring/store/getters_spec.js +++ b/spec/frontend/monitoring/store/getters_spec.js @@ -327,7 +327,8 @@ describe('Monitoring store Getters', () => { describe('getCustomVariablesArray', () => { let state; const sampleVariables = { - label1: 'pod', + 'var-label1': 'pod', + 'var-label2': 'env', }; beforeEach(() => { @@ -340,7 +341,7 @@ describe('Monitoring store Getters', () => { mutations[types.SET_PROM_QUERY_VARIABLES](state, sampleVariables); const variablesArray = getters.getCustomVariablesArray(state); - expect(variablesArray).toEqual(['label1', 'pod']); + expect(variablesArray).toEqual(['label1', 'pod', 'label2', 'env']); }); it('transforms the promVariables object to an empty array when no keys are present', () => { diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index 21597033e0a..52fd776e67e 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -192,9 +192,10 @@ describe('monitoring/utils', () => { direction: 'left', anchor: 'top', pod: 'POD', + 'var-pod': 'POD', }); - expect(promCustomVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' })); + expect(promCustomVariablesFromUrl()).toEqual(expect.objectContaining({ 'var-pod': 'POD' })); }); it('returns an empty object when no custom variables are present', () => { diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js index f7b1f041ef2..dd24ecf707d 100644 --- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js +++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js @@ -2,10 +2,7 @@ import Vue from 'vue'; import { mount } from '@vue/test-utils'; import { formatDate } from '~/lib/utils/datetime_utility'; import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; -import { - defaultAssignees, - defaultMilestone, -} from '../../../../javascripts/vue_shared/components/issue/related_issuable_mock_data'; +import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data'; describe('RelatedIssuableItem', () => { let wrapper; diff --git a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js index b7de40b4831..34ccdf38b00 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js @@ -37,10 +37,10 @@ const MOCK_DATA = { noteHtml: ` <div class="suggestion"> <div class="line">-oldtest</div> - </div> + </div> <div class="suggestion"> <div class="line">+newtest</div> - </div> + </div> `, isApplied: false, helpPagePath: 'path_to_docs', @@ -59,7 +59,7 @@ describe('Suggestion component', () => { diffTable = vm.generateDiff(0).$mount().$el; - spyOn(vm, 'renderSuggestions'); + jest.spyOn(vm, 'renderSuggestions').mockImplementation(() => {}); vm.renderSuggestions(); Vue.nextTick(done); }); @@ -85,10 +85,6 @@ describe('Suggestion component', () => { expect(diffTable.querySelector('.md-suggestion-diff')).not.toBeNull(); }); - it('generates a diff table that contains contents of `oldLineContent`', () => { - expect(diffTable.innerHTML.includes(vm.fromContent)).toBe(true); - }); - it('generates a diff table that contains contents the suggested lines', () => { MOCK_DATA.suggestions[0].diff_lines.forEach(line => { const text = line.text.substring(1); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js index 5d995f06abb..29bced394dc 100644 --- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js @@ -3,7 +3,7 @@ import { head } from 'lodash'; import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; import { mount, createLocalVue } from '@vue/test-utils'; -import { trimText } from 'spec/helpers/text_helper'; +import { trimText } from 'helpers/text_helper'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; @@ -12,7 +12,6 @@ const localVue = createLocalVue(); describe('ProjectSelector component', () => { let wrapper; let vm; - loadJSONFixtures('static/projects.json'); const allProjects = getJSONFixture('static/projects.json'); const searchResults = allProjects.slice(0, 5); let selected = []; @@ -21,9 +20,6 @@ describe('ProjectSelector component', () => { const findSearchInput = () => wrapper.find(GlSearchBoxByType).find('input'); beforeEach(() => { - jasmine.clock().install(); - jasmine.clock().mockDate(); - wrapper = mount(Vue.extend(ProjectSelector), { localVue, propsData: { @@ -41,7 +37,6 @@ describe('ProjectSelector component', () => { }); afterEach(() => { - jasmine.clock().uninstall(); vm.$destroy(); }); @@ -49,42 +44,17 @@ describe('ProjectSelector component', () => { expect(wrapper.findAll('.js-project-list-item').length).toBe(5); }); - it(`triggers a (debounced) search when the search input value changes`, () => { - spyOn(vm, '$emit'); + it(`triggers a search when the search input value changes`, () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); const query = 'my test query!'; const searchInput = findSearchInput(); searchInput.setValue(query); searchInput.trigger('input'); - expect(vm.$emit).not.toHaveBeenCalledWith(); - jasmine.clock().tick(501); - expect(vm.$emit).toHaveBeenCalledWith('searched', query); }); - it(`debounces the search input`, () => { - spyOn(vm, '$emit'); - const searchInput = findSearchInput(); - - const updateSearchQuery = (count = 0) => { - if (count === 10) { - jasmine.clock().tick(101); - - expect(vm.$emit).toHaveBeenCalledTimes(1); - expect(vm.$emit).toHaveBeenCalledWith('searched', `search query #9`); - } else { - searchInput.setValue(`search query #${count}`); - searchInput.trigger('input'); - - jasmine.clock().tick(400); - updateSearchQuery(count + 1); - } - }; - - updateSearchQuery(); - }); - it(`includes a placeholder in the search box`, () => { const searchInput = findSearchInput(); @@ -92,14 +62,14 @@ describe('ProjectSelector component', () => { }); it(`triggers a "bottomReached" event when user has scrolled to the bottom of the list`, () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); wrapper.find(GlInfiniteScroll).vm.$emit('bottomReached'); expect(vm.$emit).toHaveBeenCalledWith('bottomReached'); }); it(`triggers a "projectClicked" event when a project is clicked`, () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); wrapper.find(ProjectListItem).vm.$emit('click', head(searchResults)); expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults)); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js deleted file mode 100644 index ee6c2e2cc46..00000000000 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue'; - -describe('User Avatar Svg Component', () => { - describe('Initialization', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallowMount(UserAvatarSvg, { - propsData: { - size: 99, - svg: - '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M1.707 15.707C1.077z"/></svg>', - }, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('should have <svg> as a child element', () => { - expect(wrapper.element.tagName).toEqual('svg'); - expect(wrapper.html()).toContain('<path'); - }); - }); -}); diff --git a/spec/graphql/mutations/design_management/delete_spec.rb b/spec/graphql/mutations/design_management/delete_spec.rb new file mode 100644 index 00000000000..df587be5089 --- /dev/null +++ b/spec/graphql/mutations/design_management/delete_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mutations::DesignManagement::Delete do + include DesignManagementTestHelpers + + let(:issue) { create(:issue) } + let(:current_designs) { issue.designs.current } + let(:user) { issue.author } + let(:project) { issue.project } + let(:design_a) { create(:design, :with_file, issue: issue) } + let(:design_b) { create(:design, :with_file, issue: issue) } + let(:design_c) { create(:design, :with_file, issue: issue) } + let(:filenames) { [design_a, design_b, design_c].map(&:filename) } + + let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + + before do + # TODO these tests are being temporarily skipped unless run in EE, + # as we are in the process of moving Design Management to FOSS in 13.0 + # in steps. In the current step the services have not yet been moved, + # which are used by this mutation. + # + # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283. + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + stub_const('Errors', Gitlab::Graphql::Errors, transfer_nested_constants: true) + end + + def run_mutation + mutation = described_class.new(object: nil, context: { current_user: user }, field: nil) + mutation.resolve(project_path: project.full_path, iid: issue.iid, filenames: filenames) + end + + describe '#resolve' do + let(:expected_response) do + { errors: [], version: DesignManagement::Version.for_issue(issue).ordered.first } + end + + shared_examples "failures" do |error: Gitlab::Graphql::Errors::ResourceNotAvailable| + it "raises #{error.name}" do + expect { run_mutation }.to raise_error(error) + end + end + + shared_examples "resource not available" do + it_behaves_like "failures" + end + + context "when the feature is not available" do + before do + enable_design_management(false) + end + + it_behaves_like "resource not available" + end + + context "when the feature is available" do + before do + enable_design_management(true) + end + + context "when the user is not allowed to delete designs" do + let(:user) { create(:user) } + + it_behaves_like "resource not available" + end + + context 'deleting an already deleted file' do + before do + run_mutation + end + + it 'fails with an argument error' do + expect { run_mutation }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end + end + + context "when deleting all the designs" do + let(:response) { run_mutation } + + it "returns a new version, and no errors" do + expect(response).to include(expected_response) + end + + describe 'the current designs' do + before do + run_mutation + end + + it 'is empty' do + expect(current_designs).to be_empty + end + end + + it 'runs no more than 28 queries' do + filenames.each(&:present?) # ignore setup + # Queries: as of 2019-08-28 + # ------------- + # 01. routing query + # 02. find project by id + # 03. project.project_features + # 04. find namespace by id and type + # 05,06. project.authorizations for user (same query twice) + # 07. find issue by iid + # 08. find project by id + # 09. find namespace by id + # 10. find group namespace by id + # 11. project.authorizations for user (same query as 5) + # 12. project.project_features (same query as 3) + # 13. project.authorizations for user (same query as 5) + # 14. current designs by filename and issue + # 15, 16 project.authorizations for user (same query as 5) + # 17. find route by id and source_type + # ------------- our queries are below: + # 18. start transaction 1 + # 19. start transaction 2 + # 20. find version by sha and issue + # 21. exists version with sha and issue? + # 22. leave transaction 2 + # 23. create version with sha and issue + # 24. create design-version links + # 25. validate version.actions.present? + # 26. validate version.issue.present? + # 27. validate version.sha is unique + # 28. leave transaction 1 + # + expect { run_mutation }.not_to exceed_query_limit(28) + end + end + + context "when deleting a design" do + let(:filenames) { [design_a.filename] } + let(:response) { run_mutation } + + it "returns the expected response" do + expect(response).to include(expected_response) + end + + describe 'the current designs' do + before do + run_mutation + end + + it 'does contain designs b and c' do + expect(current_designs).to contain_exactly(design_b, design_c) + end + end + end + end + end +end diff --git a/spec/graphql/mutations/design_management/upload_spec.rb b/spec/graphql/mutations/design_management/upload_spec.rb new file mode 100644 index 00000000000..10c2164caec --- /dev/null +++ b/spec/graphql/mutations/design_management/upload_spec.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Mutations::DesignManagement::Upload do + include DesignManagementTestHelpers + include ConcurrentHelpers + + let(:issue) { create(:issue) } + let(:user) { issue.author } + let(:project) { issue.project } + + subject(:mutation) do + described_class.new(object: nil, context: { current_user: user }, field: nil) + end + + def run_mutation(files_to_upload = files, project_path = project.full_path, iid = issue.iid) + mutation = described_class.new(object: nil, context: { current_user: user }, field: nil) + mutation.resolve(project_path: project_path, iid: iid, files: files_to_upload) + end + + describe "#resolve" do + let(:files) { [fixture_file_upload('spec/fixtures/dk.png')] } + + subject(:resolve) do + mutation.resolve(project_path: project.full_path, iid: issue.iid, files: files) + end + + shared_examples "resource not available" do + it "raises an error" do + expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context "when the feature is not available" do + it_behaves_like "resource not available" + end + + context "when the feature is available" do + before do + # TODO these tests are being temporarily skipped unless run in EE, + # as we are in the process of moving Design Management to FOSS in 13.0 + # in steps. In the current step the services have not yet been moved, + # which are used by this mutation. + # + # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283. + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + enable_design_management + end + + describe 'contention in the design repo' do + before do + issue.design_collection.repository.create_if_not_exists + end + + let(:files) do + ['dk.png', 'rails_sample.jpg', 'banana_sample.gif'] + .cycle + .take(Concurrent.processor_count * 2) + .map { |f| RenameableUpload.unique_file(f) } + end + + def creates_designs + prior_count = DesignManagement::Design.count + + expect { yield }.not_to raise_error + + expect(DesignManagement::Design.count).to eq(prior_count + files.size) + end + + describe 'running requests in parallel' do + it 'does not cause errors' do + creates_designs do + run_parallel(files.map { |f| -> { run_mutation([f]) } }) + end + end + end + + describe 'running requests in parallel on different issues' do + it 'does not cause errors' do + creates_designs do + issues = create_list(:issue, files.size, author: user) + issues.each { |i| i.project.add_developer(user) } + blocks = files.zip(issues).map do |(f, i)| + -> { run_mutation([f], i.project.full_path, i.iid) } + end + + run_parallel(blocks) + end + end + end + + describe 'running requests in serial' do + it 'does not cause errors' do + creates_designs do + files.each do |f| + run_mutation([f]) + end + end + end + end + end + + context "when the user is not allowed to upload designs" do + let(:user) { create(:user) } + + it_behaves_like "resource not available" + end + + context "a valid design" do + it "returns the updated designs" do + expect(resolve[:errors]).to eq [] + expect(resolve[:designs].map(&:filename)).to contain_exactly("dk.png") + end + end + + context "context when passing an invalid project" do + let(:project) { build(:project) } + + it_behaves_like "resource not available" + end + + context "context when passing an invalid issue" do + let(:issue) { build(:issue) } + + it_behaves_like "resource not available" + end + + context "when creating designs causes errors" do + before do + fake_service = double(::DesignManagement::SaveDesignsService) + + allow(fake_service).to receive(:execute).and_return(status: :error, message: "Something failed") + allow(::DesignManagement::SaveDesignsService).to receive(:new).and_return(fake_service) + end + + it "wraps the errors" do + expect(resolve[:errors]).to eq(["Something failed"]) + expect(resolve[:designs]).to eq([]) + end + end + end + end +end diff --git a/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb new file mode 100644 index 00000000000..a5054ae3ebf --- /dev/null +++ b/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::DesignManagement::DesignAtVersionResolver do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:issue) { create(:issue) } + let_it_be(:project) { issue.project } + let_it_be(:user) { create(:user) } + let_it_be(:design_a) { create(:design, issue: issue) } + let_it_be(:version_a) { create(:design_version, issue: issue, created_designs: [design_a]) } + + let(:current_user) { user } + let(:object) { issue.design_collection } + let(:global_id) { GitlabSchema.id_from_object(design_at_version).to_s } + + let(:design_at_version) { ::DesignManagement::DesignAtVersion.new(design: design_a, version: version_a) } + + let(:resource_not_available) { ::Gitlab::Graphql::Errors::ResourceNotAvailable } + + before do + enable_design_management + project.add_developer(user) + end + + describe '#resolve' do + context 'when the user cannot see designs' do + let(:current_user) { create(:user) } + + it 'raises ResourceNotAvailable' do + expect { resolve_design }.to raise_error(resource_not_available) + end + end + + it 'returns the specified design' do + expect(resolve_design).to eq(design_at_version) + end + + context 'the ID belongs to a design on another issue' do + let(:other_dav) do + create(:design_at_version, issue: create(:issue, project: project)) + end + + let(:global_id) { global_id_of(other_dav) } + + it 'raises ResourceNotAvailable' do + expect { resolve_design }.to raise_error(resource_not_available) + end + + context 'the current object does not constrain the issue' do + let(:object) { nil } + + it 'returns the object' do + expect(resolve_design).to eq(other_dav) + end + end + end + end + + private + + def resolve_design + args = { id: global_id } + ctx = { current_user: current_user } + eager_resolve(described_class, obj: object, args: args, ctx: ctx) + end +end diff --git a/spec/graphql/resolvers/design_management/design_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_resolver_spec.rb new file mode 100644 index 00000000000..857acc3d371 --- /dev/null +++ b/spec/graphql/resolvers/design_management/design_resolver_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::DesignManagement::DesignResolver do + include GraphqlHelpers + include DesignManagementTestHelpers + + before do + enable_design_management + end + + describe '#resolve' do + let_it_be(:issue) { create(:issue) } + let_it_be(:project) { issue.project } + let_it_be(:first_version) { create(:design_version) } + let_it_be(:first_design) { create(:design, issue: issue, versions: [first_version]) } + let_it_be(:current_user) { create(:user) } + let_it_be(:design_on_other_issue) do + create(:design, issue: create(:issue, project: project), versions: [create(:design_version)]) + end + + let(:args) { { id: GitlabSchema.id_from_object(first_design).to_s } } + let(:gql_context) { { current_user: current_user } } + + before do + project.add_developer(current_user) + end + + context 'when the user cannot see designs' do + let(:gql_context) { { current_user: create(:user) } } + + it 'returns nothing' do + expect(resolve_design).to be_nil + end + end + + context 'when no argument has been passed' do + let(:args) { {} } + + it 'raises an error' do + expect { resolve_design }.to raise_error(::Gitlab::Graphql::Errors::ArgumentError, /must/) + end + end + + context 'when both arguments have been passed' do + let(:args) { { filename: first_design.filename, id: GitlabSchema.id_from_object(first_design).to_s } } + + it 'raises an error' do + expect { resolve_design }.to raise_error(::Gitlab::Graphql::Errors::ArgumentError, /may/) + end + end + + context 'by ID' do + it 'returns the specified design' do + expect(resolve_design).to eq(first_design) + end + + context 'the ID belongs to a design on another issue' do + let(:args) { { id: GitlabSchema.id_from_object(design_on_other_issue).to_s } } + + it 'returns nothing' do + expect(resolve_design).to be_nil + end + end + end + + context 'by filename' do + let(:args) { { filename: first_design.filename } } + + it 'returns the specified design' do + expect(resolve_design).to eq(first_design) + end + + context 'the filename belongs to a design on another issue' do + let(:args) { { filename: design_on_other_issue.filename } } + + it 'returns nothing' do + expect(resolve_design).to be_nil + end + end + end + end + + def resolve_design + resolve(described_class, obj: issue.design_collection, args: args, ctx: gql_context) + end +end diff --git a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb new file mode 100644 index 00000000000..28fc9e2151d --- /dev/null +++ b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::DesignManagement::DesignsResolver do + include GraphqlHelpers + include DesignManagementTestHelpers + + before do + enable_design_management + end + + describe '#resolve' do + let_it_be(:issue) { create(:issue) } + let_it_be(:project) { issue.project } + let_it_be(:first_version) { create(:design_version) } + let_it_be(:first_design) { create(:design, issue: issue, versions: [first_version]) } + let_it_be(:current_user) { create(:user) } + let(:gql_context) { { current_user: current_user } } + let(:args) { {} } + + before do + project.add_developer(current_user) + end + + context 'when the user cannot see designs' do + let(:gql_context) { { current_user: create(:user) } } + + it 'returns nothing' do + expect(resolve_designs).to be_empty + end + end + + context 'for a design collection' do + context 'which contains just a single design' do + it 'returns just that design' do + expect(resolve_designs).to contain_exactly(first_design) + end + end + + context 'which contains another design' do + it 'returns all designs' do + second_version = create(:design_version) + second_design = create(:design, issue: issue, versions: [second_version]) + + expect(resolve_designs).to contain_exactly(first_design, second_design) + end + end + end + + describe 'filtering' do + describe 'by filename' do + let(:second_version) { create(:design_version) } + let(:second_design) { create(:design, issue: issue, versions: [second_version]) } + let(:args) { { filenames: [second_design.filename] } } + + it 'resolves to just the relevant design, ignoring designs with the same filename on different issues' do + create(:design, issue: create(:issue, project: project), filename: second_design.filename) + + expect(resolve_designs).to contain_exactly(second_design) + end + end + + describe 'by id' do + let(:second_version) { create(:design_version) } + let(:second_design) { create(:design, issue: issue, versions: [second_version]) } + + context 'the ID is on the current issue' do + let(:args) { { ids: [GitlabSchema.id_from_object(second_design).to_s] } } + + it 'resolves to just the relevant design' do + expect(resolve_designs).to contain_exactly(second_design) + end + end + + context 'the ID is on a different issue' do + let(:third_version) { create(:design_version) } + let(:third_design) { create(:design, issue: create(:issue, project: project), versions: [third_version]) } + + let(:args) { { ids: [GitlabSchema.id_from_object(third_design).to_s] } } + + it 'ignores it' do + expect(resolve_designs).to be_empty + end + end + end + end + end + + def resolve_designs + resolve(described_class, obj: issue.design_collection, args: args, ctx: gql_context) + end +end diff --git a/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb b/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb new file mode 100644 index 00000000000..cc9c0436885 --- /dev/null +++ b/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::DesignManagement::Version::DesignAtVersionResolver do + include GraphqlHelpers + + include_context 'four designs in three versions' + + let(:current_user) { authorized_user } + let(:gql_context) { { current_user: current_user } } + + let(:version) { third_version } + let(:design) { design_a } + + let(:all_singular_args) do + { + design_at_version_id: global_id_of(dav(design)), + design_id: global_id_of(design), + filename: design.filename + } + end + + shared_examples 'a bad argument' do + let(:err_class) { ::Gitlab::Graphql::Errors::ArgumentError } + + it 'raises an appropriate error' do + expect { resolve_objects }.to raise_error(err_class) + end + end + + describe '#resolve' do + describe 'passing combinations of arguments' do + context 'passing no arguments' do + let(:args) { {} } + + it_behaves_like 'a bad argument' + end + + context 'passing all arguments' do + let(:args) { all_singular_args } + + it_behaves_like 'a bad argument' + end + + context 'passing any two arguments' do + let(:args) { all_singular_args.slice(*all_singular_args.keys.sample(2)) } + + it_behaves_like 'a bad argument' + end + end + + %i[design_at_version_id design_id filename].each do |arg| + describe "passing #{arg}" do + let(:args) { all_singular_args.slice(arg) } + + it 'finds the design' do + expect(resolve_objects).to eq(dav(design)) + end + + context 'when the user cannot see designs' do + let(:current_user) { create(:user) } + + it 'returns nothing' do + expect(resolve_objects).to be_nil + end + end + end + end + + describe 'attempting to retrieve an object not visible at this version' do + let(:design) { design_d } + + %i[design_at_version_id design_id filename].each do |arg| + describe "passing #{arg}" do + let(:args) { all_singular_args.slice(arg) } + + it 'does not find the design' do + expect(resolve_objects).to be_nil + end + end + end + end + end + + def resolve_objects + resolve(described_class, obj: version, args: args, ctx: gql_context) + end + + def dav(design) + build(:design_at_version, design: design, version: version) + end +end diff --git a/spec/graphql/resolvers/design_management/version/designs_at_version_resolver_spec.rb b/spec/graphql/resolvers/design_management/version/designs_at_version_resolver_spec.rb new file mode 100644 index 00000000000..123b26862d0 --- /dev/null +++ b/spec/graphql/resolvers/design_management/version/designs_at_version_resolver_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::DesignManagement::Version::DesignsAtVersionResolver do + include GraphqlHelpers + + include_context 'four designs in three versions' + + let_it_be(:current_user) { authorized_user } + let(:gql_context) { { current_user: current_user } } + + let(:version) { third_version } + + describe '.single' do + let(:single) { ::Resolvers::DesignManagement::Version::DesignAtVersionResolver } + + it 'returns the single context resolver' do + expect(described_class.single).to eq(single) + end + end + + describe '#resolve' do + let(:args) { {} } + + context 'when the user cannot see designs' do + let(:current_user) { create(:user) } + + it 'returns nothing' do + expect(resolve_objects).to be_empty + end + end + + context 'for the current version' do + it 'returns all designs visible at that version' do + expect(resolve_objects).to contain_exactly(dav(design_a), dav(design_b), dav(design_c)) + end + end + + context 'for a previous version with more objects' do + let(:version) { second_version } + + it 'returns objects that were later deleted' do + expect(resolve_objects).to contain_exactly(dav(design_a), dav(design_b), dav(design_c), dav(design_d)) + end + end + + context 'for a previous version with fewer objects' do + let(:version) { first_version } + + it 'does not return objects that were later created' do + expect(resolve_objects).to contain_exactly(dav(design_a)) + end + end + + describe 'filtering' do + describe 'by filename' do + let(:red_herring) { create(:design, issue: create(:issue, project: project)) } + let(:args) { { filenames: [design_b.filename, red_herring.filename] } } + + it 'resolves to just the relevant design' do + create(:design, issue: create(:issue, project: project), filename: design_b.filename) + + expect(resolve_objects).to contain_exactly(dav(design_b)) + end + end + + describe 'by id' do + let(:red_herring) { create(:design, issue: create(:issue, project: project)) } + let(:args) { { ids: [design_a, red_herring].map { |x| global_id_of(x) } } } + + it 'resolves to just the relevant design, ignoring objects on other issues' do + expect(resolve_objects).to contain_exactly(dav(design_a)) + end + end + end + end + + def resolve_objects + resolve(described_class, obj: version, args: args, ctx: gql_context) + end + + def dav(design) + build(:design_at_version, design: design, version: version) + end +end diff --git a/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb new file mode 100644 index 00000000000..ef50598d241 --- /dev/null +++ b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::DesignManagement::VersionInCollectionResolver do + include GraphqlHelpers + include DesignManagementTestHelpers + + let(:resolver) { described_class } + + describe '#resolve' do + let_it_be(:issue) { create(:issue) } + let_it_be(:current_user) { create(:user) } + let_it_be(:first_version) { create(:design_version, issue: issue) } + + let(:project) { issue.project } + let(:params) { {} } + + before do + enable_design_management + project.add_developer(current_user) + end + + let(:appropriate_error) { ::Gitlab::Graphql::Errors::ArgumentError } + + subject(:result) { resolve_version(issue.design_collection) } + + context 'Neither id nor sha is passed as parameters' do + it 'raises an appropriate error' do + expect { result }.to raise_error(appropriate_error) + end + end + + context 'we pass an id' do + let(:params) { { id: global_id_of(first_version) } } + + it { is_expected.to eq(first_version) } + end + + context 'we pass a sha' do + let(:params) { { sha: first_version.sha } } + + it { is_expected.to eq(first_version) } + end + + context 'we pass an inconsistent mixture of sha and version id' do + let(:params) { { sha: first_version.sha, id: global_id_of(create(:design_version)) } } + + it { is_expected.to be_nil } + end + + context 'we pass the id of something that is not a design_version' do + let(:params) { { id: global_id_of(project) } } + + it 'raises an appropriate error' do + expect { result }.to raise_error(appropriate_error) + end + end + end + + def resolve_version(obj, context = { current_user: current_user }) + resolve(resolver, obj: obj, args: params, ctx: context) + end +end diff --git a/spec/graphql/resolvers/design_management/version_resolver_spec.rb b/spec/graphql/resolvers/design_management/version_resolver_spec.rb new file mode 100644 index 00000000000..e7c09351204 --- /dev/null +++ b/spec/graphql/resolvers/design_management/version_resolver_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::DesignManagement::VersionResolver do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:issue) { create(:issue) } + let_it_be(:current_user) { create(:user) } + let_it_be(:version) { create(:design_version, issue: issue) } + let_it_be(:developer) { create(:user) } + + let(:project) { issue.project } + let(:params) { { id: global_id_of(version) } } + + before do + enable_design_management + project.add_developer(developer) + end + + context 'the current user is not authorized' do + let(:current_user) { create(:user) } + + it 'raises an error on resolution' do + expect { resolve_version }.to raise_error(::Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'the current user is authorized' do + let(:current_user) { developer } + + context 'the id parameter is provided' do + it 'returns the specified version' do + expect(resolve_version).to eq(version) + end + end + end + + def resolve_version + resolve(described_class, obj: nil, args: params, ctx: { current_user: current_user }) + end +end diff --git a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb new file mode 100644 index 00000000000..d5bab025e45 --- /dev/null +++ b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::DesignManagement::VersionsResolver do + include GraphqlHelpers + include DesignManagementTestHelpers + + describe '#resolve' do + let(:resolver) { described_class } + let_it_be(:issue) { create(:issue) } + let_it_be(:authorized_user) { create(:user) } + let_it_be(:first_version) { create(:design_version, issue: issue) } + let_it_be(:other_version) { create(:design_version, issue: issue) } + let_it_be(:first_design) { create(:design, issue: issue, versions: [first_version, other_version]) } + let_it_be(:other_design) { create(:design, :with_versions, issue: issue) } + + let(:project) { issue.project } + let(:params) { {} } + let(:current_user) { authorized_user } + let(:parent_args) { { irrelevant: 1.2 } } + let(:parent) { double('Parent', parent: nil, irep_node: double(arguments: parent_args)) } + + before do + enable_design_management + project.add_developer(authorized_user) + end + + shared_examples 'a source of versions' do + subject(:result) { resolve_versions(object) } + + let_it_be(:all_versions) { object.versions.ordered } + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it { is_expected.to be_empty } + end + + context 'without constraints' do + it 'returns the ordered versions' do + expect(result).to eq(all_versions) + end + end + + context 'when constrained' do + let_it_be(:matching) { all_versions.earlier_or_equal_to(first_version) } + + shared_examples 'a query for all_versions up to the first_version' do + it { is_expected.to eq(matching) } + end + + context 'by earlier_or_equal_to_id' do + let(:params) { { id: global_id_of(first_version) } } + + it_behaves_like 'a query for all_versions up to the first_version' + end + + context 'by earlier_or_equal_to_sha' do + let(:params) { { sha: first_version.sha } } + + it_behaves_like 'a query for all_versions up to the first_version' + end + + context 'by earlier_or_equal_to_sha AND earlier_or_equal_to_id' do + context 'and they match' do + # This usage is rather dumb, but so long as they match, this will + # return successfully + let(:params) do + { + sha: first_version.sha, + id: global_id_of(first_version) + } + end + + it_behaves_like 'a query for all_versions up to the first_version' + end + + context 'and they do not match' do + let(:params) do + { + sha: first_version.sha, + id: global_id_of(other_version) + } + end + + it 'raises a suitable error' do + expect { result }.to raise_error(GraphQL::ExecutionError) + end + end + end + + context 'by at_version in parent' do + let(:parent_args) { { atVersion: global_id_of(first_version) } } + + it_behaves_like 'a query for all_versions up to the first_version' + end + end + end + + describe 'a design collection' do + let_it_be(:object) { DesignManagement::DesignCollection.new(issue) } + + it_behaves_like 'a source of versions' + end + + describe 'a design' do + let_it_be(:object) { first_design } + + it_behaves_like 'a source of versions' + end + + def resolve_versions(obj, context = { current_user: current_user }) + eager_resolve(resolver, obj: obj, args: params.merge(parent: parent), ctx: context) + end + end +end diff --git a/spec/graphql/types/design_management/design_at_version_type_spec.rb b/spec/graphql/types/design_management/design_at_version_type_spec.rb new file mode 100644 index 00000000000..1453d73d59c --- /dev/null +++ b/spec/graphql/types/design_management/design_at_version_type_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['DesignAtVersion'] do + it_behaves_like 'a GraphQL type with design fields' do + let(:extra_design_fields) { %i[version design] } + let_it_be(:design) { create(:design, :with_versions) } + let(:object_id) do + version = design.versions.first + GitlabSchema.id_from_object(create(:design_at_version, design: design, version: version)) + end + let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design_at_version)) } + let(:object_type) { ::Types::DesignManagement::DesignAtVersionType } + end +end diff --git a/spec/graphql/types/design_management/design_collection_type_spec.rb b/spec/graphql/types/design_management/design_collection_type_spec.rb new file mode 100644 index 00000000000..65150f0971d --- /dev/null +++ b/spec/graphql/types/design_management/design_collection_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['DesignCollection'] do + it { expect(described_class).to require_graphql_authorizations(:read_design) } + + it 'has the expected fields' do + expected_fields = %i[project issue designs versions version designAtVersion design] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/design_management/design_type_spec.rb b/spec/graphql/types/design_management/design_type_spec.rb new file mode 100644 index 00000000000..75b4cd66d5e --- /dev/null +++ b/spec/graphql/types/design_management/design_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['Design'] do + it_behaves_like 'a GraphQL type with design fields' do + let(:extra_design_fields) { %i[notes discussions versions] } + let_it_be(:design) { create(:design, :with_versions) } + let(:object_id) { GitlabSchema.id_from_object(design) } + let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design, :with_versions)) } + let(:object_type) { ::Types::DesignManagement::DesignType } + end +end diff --git a/spec/graphql/types/design_management/design_version_event_enum_spec.rb b/spec/graphql/types/design_management/design_version_event_enum_spec.rb new file mode 100644 index 00000000000..a65f1bb5990 --- /dev/null +++ b/spec/graphql/types/design_management/design_version_event_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['DesignVersionEvent'] do + it { expect(described_class.graphql_name).to eq('DesignVersionEvent') } + + it 'exposes the correct event states' do + expect(described_class.values.keys).to include(*%w[CREATION MODIFICATION DELETION NONE]) + end +end diff --git a/spec/graphql/types/design_management/version_type_spec.rb b/spec/graphql/types/design_management/version_type_spec.rb new file mode 100644 index 00000000000..3317c4c6571 --- /dev/null +++ b/spec/graphql/types/design_management/version_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['DesignVersion'] do + it { expect(described_class).to require_graphql_authorizations(:read_design) } + + it 'has the expected fields' do + expected_fields = %i[id sha designs design_at_version designs_at_version] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/design_management_type_spec.rb b/spec/graphql/types/design_management_type_spec.rb new file mode 100644 index 00000000000..a6204f20f23 --- /dev/null +++ b/spec/graphql/types/design_management_type_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['DesignManagement'] do + it { expect(described_class).to have_graphql_fields(:version, :design_at_version) } +end diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index d51457b29b4..a8f7edcfe8e 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -14,7 +14,8 @@ describe GitlabSchema.types['Issue'] do it 'has specific fields' do fields = %i[iid title description state reference author assignees participants labels milestone due_date confidential discussion_locked upvotes downvotes user_notes_count web_path web_url relative_position - subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status] + subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status + designs design_collection] fields.each do |field_name| expect(described_class).to have_graphql_field(field_name) diff --git a/spec/graphql/types/notes/noteable_type_spec.rb b/spec/graphql/types/notes/noteable_type_spec.rb index 3176134fa14..4a81f45bd4e 100644 --- a/spec/graphql/types/notes/noteable_type_spec.rb +++ b/spec/graphql/types/notes/noteable_type_spec.rb @@ -8,6 +8,7 @@ describe Types::Notes::NoteableType do it 'knows the correct type for objects' do expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType) expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType) + expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType) end end end diff --git a/spec/graphql/types/permission_types/issue_spec.rb b/spec/graphql/types/permission_types/issue_spec.rb index a94bc6b780e..a7a3dd00f11 100644 --- a/spec/graphql/types/permission_types/issue_spec.rb +++ b/spec/graphql/types/permission_types/issue_spec.rb @@ -5,8 +5,9 @@ require 'spec_helper' describe Types::PermissionTypes::Issue do it do expected_permissions = [ - :read_issue, :admin_issue, :update_issue, - :create_note, :reopen_issue + :read_issue, :admin_issue, :update_issue, :reopen_issue, + :read_design, :create_design, :destroy_design, + :create_note ] expected_permissions.each do |permission| diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb index 56c4c2de4df..2789464d29c 100644 --- a/spec/graphql/types/permission_types/project_spec.rb +++ b/spec/graphql/types/permission_types/project_spec.rb @@ -13,7 +13,7 @@ describe Types::PermissionTypes::Project do :create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch, :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content, - :read_merge_request + :read_merge_request, :read_design, :create_design, :destroy_design ] expected_permissions.each do |permission| diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index ab210f2e918..1f269a80d00 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -8,7 +8,7 @@ describe GitlabSchema.types['Query'] do end it 'has the expected fields' do - expected_fields = %i[project namespace group echo metadata current_user snippets] + expected_fields = %i[project namespace group echo metadata current_user snippets design_management] expect(described_class).to have_graphql_fields(*expected_fields).at_least end diff --git a/spec/lib/gitlab/git_access_design_spec.rb b/spec/lib/gitlab/git_access_design_spec.rb index b09afc67c90..d816608f7e5 100644 --- a/spec/lib/gitlab/git_access_design_spec.rb +++ b/spec/lib/gitlab/git_access_design_spec.rb @@ -21,16 +21,7 @@ describe Gitlab::GitAccessDesign do end context 'when the user is allowed to manage designs' do - # TODO This test is being temporarily skipped unless run in EE, - # as we are in the process of moving Design Management to FOSS in 13.0 - # in steps. In the current step the policies have not yet been moved - # which means that although the `GitAccessDesign` class has moved, the - # user will always be denied access in FOSS. - # - # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283. it do - skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? - is_expected.to be_a(::Gitlab::GitAccessResult::Success) end end diff --git a/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb b/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb new file mode 100644 index 00000000000..b4b1d8bec51 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Kubernetes::Helm::Parsers::ListV2 do + let(:valid_file_contents) do + <<~EOF + { + "Next": "", + "Releases": [ + { + "Name": "certmanager", + "Revision": 2, + "Updated": "Sun Mar 29 06:55:42 2020", + "Status": "DEPLOYED", + "Chart": "cert-manager-v0.10.1", + "AppVersion": "v0.10.1", + "Namespace": "gitlab-managed-apps" + }, + { + "Name": "certmanager-crds", + "Revision": 2, + "Updated": "Sun Mar 29 06:55:32 2020", + "Status": "DEPLOYED", + "Chart": "cert-manager-crds-v0.2.0", + "AppVersion": "release-0.10", + "Namespace": "gitlab-managed-apps" + }, + { + "Name": "certmanager-issuer", + "Revision": 1, + "Updated": "Tue Feb 18 10:04:04 2020", + "Status": "FAILED", + "Chart": "cert-manager-issuer-v0.1.0", + "AppVersion": "", + "Namespace": "gitlab-managed-apps" + }, + { + "Name": "runner", + "Revision": 2, + "Updated": "Sun Mar 29 07:01:01 2020", + "Status": "DEPLOYED", + "Chart": "gitlab-runner-0.14.0", + "AppVersion": "12.8.0", + "Namespace": "gitlab-managed-apps" + } + ] + } + EOF + end + + describe '#initialize' do + it 'initializes without error' do + expect do + described_class.new(valid_file_contents) + end.not_to raise_error + end + + it 'raises an error on invalid JSON' do + expect do + described_class.new('') + end.to raise_error(described_class::ParserError, 'A JSON text must at least contain two octets!') + end + end + + describe '#releases' do + subject(:list_v2) { described_class.new(valid_file_contents) } + + it 'returns list of releases' do + expect(list_v2.releases).to match([ + a_hash_including('Name' => 'certmanager', 'Status' => 'DEPLOYED'), + a_hash_including('Name' => 'certmanager-crds', 'Status' => 'DEPLOYED'), + a_hash_including('Name' => 'certmanager-issuer', 'Status' => 'FAILED'), + a_hash_including('Name' => 'runner', 'Status' => 'DEPLOYED') + ]) + end + + context 'empty Releases' do + let(:valid_file_contents) { '{}' } + + it 'returns an empty array' do + expect(list_v2.releases).to eq([]) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index a2626f8d680..ead650a27f0 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -8,7 +8,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do before do allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric) - stub_env('enable_memory_uss_pss', "1") end describe '#initialize' do @@ -37,6 +36,21 @@ describe Gitlab::Metrics::Samplers::RubySampler do sampler.sample end + context 'when USS+PSS sampling is disabled via environment' do + before do + stub_env('enable_memory_uss_pss', "0") + end + + it 'does not sample USS or PSS' do + expect(Gitlab::Metrics::System).not_to receive(:memory_usage_uss_pss) + + expect(sampler.metrics[:process_unique_memory_bytes]).not_to receive(:set) + expect(sampler.metrics[:process_proportional_memory_bytes]).not_to receive(:set) + + sampler.sample + end + end + it 'adds a metric containing the amount of open file descriptors' do expect(Gitlab::Metrics::System).to receive(:file_descriptor_count) .and_return(4) diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb index 82deba0d92c..321e1062a96 100644 --- a/spec/requests/api/graphql/current_user/todos_query_spec.rb +++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb @@ -9,6 +9,7 @@ describe 'Query current user todos' do let_it_be(:commit_todo) { create(:on_commit_todo, user: current_user, project: create(:project, :repository)) } let_it_be(:issue_todo) { create(:todo, user: current_user, target: create(:issue)) } let_it_be(:merge_request_todo) { create(:todo, user: current_user, target: create(:merge_request)) } + let_it_be(:design_todo) { create(:todo, user: current_user, target: create(:design)) } let(:fields) do <<~QUERY @@ -34,7 +35,8 @@ describe 'Query current user todos' do is_expected.to include( a_hash_including('id' => commit_todo.to_global_id.to_s), a_hash_including('id' => issue_todo.to_global_id.to_s), - a_hash_including('id' => merge_request_todo.to_global_id.to_s) + a_hash_including('id' => merge_request_todo.to_global_id.to_s), + a_hash_including('id' => design_todo.to_global_id.to_s) ) end @@ -42,7 +44,8 @@ describe 'Query current user todos' do is_expected.to include( a_hash_including('targetType' => 'COMMIT'), a_hash_including('targetType' => 'ISSUE'), - a_hash_including('targetType' => 'MERGEREQUEST') + a_hash_including('targetType' => 'MERGEREQUEST'), + a_hash_including('targetType' => 'DESIGN') ) end end diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb new file mode 100644 index 00000000000..9ce2313f1e0 --- /dev/null +++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "deleting designs" do + include GraphqlHelpers + include DesignManagementTestHelpers + + let(:developer) { create(:user) } + let(:current_user) { developer } + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:designs) { create_designs } + let(:variables) { {} } + + let(:mutation) do + input = { + project_path: project.full_path, + iid: issue.iid, + filenames: designs.map(&:filename) + }.merge(variables) + graphql_mutation(:design_management_delete, input) + end + + let(:mutation_response) { graphql_mutation_response(:design_management_delete) } + + def mutate! + post_graphql_mutation(mutation, current_user: current_user) + end + + before do + # TODO these tests are being temporarily skipped unless run in EE, + # as we are in the process of moving Design Management to FOSS in 13.0 + # in steps. In the current step the services have not yet been moved, + # which are used by this mutation. + # + # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283. + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + enable_design_management + + project.add_developer(developer) + end + + shared_examples 'a failed request' do + let(:the_error) { be_present } + + it 'reports an error' do + mutate! + + expect(graphql_errors).to include(a_hash_including('message' => the_error)) + end + end + + context 'the designs list is empty' do + it_behaves_like 'a failed request' do + let(:designs) { [] } + let(:the_error) { a_string_matching %r/was provided invalid value/ } + end + end + + context 'the designs list contains filenames we cannot find' do + it_behaves_like 'a failed request' do + let(:designs) { %w/foo bar baz/.map { |fn| OpenStruct.new(filename: fn) } } + let(:the_error) { a_string_matching %r/filenames were not found/ } + end + end + + context 'the current user does not have developer access' do + it_behaves_like 'a failed request' do + let(:current_user) { create(:user) } + let(:the_error) { a_string_matching %r/you don't have permission/ } + end + end + + context "when the issue does not exist" do + it_behaves_like 'a failed request' do + let(:variables) { { iid: "1234567890" } } + let(:the_error) { a_string_matching %r/does not exist/ } + end + end + + context "when saving the designs raises an error" do + let(:designs) { create_designs(1) } + + it "responds with errors" do + expect_next_instance_of(::DesignManagement::DeleteDesignsService) do |service| + expect(service) + .to receive(:execute) + .and_return({ status: :error, message: "Something went wrong" }) + end + + mutate! + + expect(mutation_response).to include('errors' => include(eq "Something went wrong")) + end + end + + context 'one of the designs is already deleted' do + let(:designs) do + create_designs(2).push(create(:design, :with_file, deleted: true, issue: issue)) + end + + it 'reports an error' do + mutate! + + expect(graphql_errors).to be_present + end + end + + context 'when the user names designs to delete' do + before do + create_designs(1) + end + + let!(:designs) { create_designs(2) } + + it 'deletes the designs' do + expect { mutate! } + .to change { issue.reset.designs.current.count }.from(3).to(1) + end + + it 'has no errors' do + mutate! + + expect(mutation_response).to include('errors' => be_empty) + end + end + + private + + def create_designs(how_many = 2) + create_list(:design, how_many, :with_file, issue: issue) + end +end diff --git a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb new file mode 100644 index 00000000000..060e37f559d --- /dev/null +++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true +require "spec_helper" + +describe "uploading designs" do + include GraphqlHelpers + include DesignManagementTestHelpers + include WorkhorseHelpers + + let(:current_user) { create(:user) } + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] } + let(:variables) { {} } + + let(:mutation) do + input = { + project_path: project.full_path, + iid: issue.iid, + files: files + }.merge(variables) + graphql_mutation(:design_management_upload, input) + end + + let(:mutation_response) { graphql_mutation_response(:design_management_upload) } + + before do + # TODO these tests are being temporarily skipped unless run in EE, + # as we are in the process of moving Design Management to FOSS in 13.0 + # in steps. In the current step the services have not yet been moved, + # which are used by this mutation. + # + # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283. + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + enable_design_management + + project.add_developer(current_user) + end + + it "returns an error if the user is not allowed to upload designs" do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).to be_present + end + + it "succeeds (backward compatibility)" do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).not_to be_present + end + + it 'succeeds' do + file_path_in_params = ['designManagementUploadInput', 'files', 0] + params = mutation_to_apollo_uploads_param(mutation, files: [file_path_in_params]) + + workhorse_post_with_file(api('/', current_user, version: 'graphql'), + params: params, + file_key: '1' + ) + + expect(graphql_errors).not_to be_present + end + + it "responds with the created designs" do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to include( + "designs" => a_collection_containing_exactly( + a_hash_including("filename" => "dk.png") + ) + ) + end + + it "can respond with skipped designs" do + 2.times do + post_graphql_mutation(mutation, current_user: current_user) + files.each(&:rewind) + end + + expect(mutation_response).to include( + "skippedDesigns" => a_collection_containing_exactly( + a_hash_including("filename" => "dk.png") + ) + ) + end + + context "when the issue does not exist" do + let(:variables) { { iid: "123" } } + + it "returns an error" do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + end + + context "when saving the designs raises an error" do + it "responds with errors" do + expect_next_instance_of(::DesignManagement::SaveDesignsService) do |service| + expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" }) + end + + post_graphql_mutation(mutation, current_user: current_user) + expect(mutation_response["errors"].first).to eq("Something went wrong") + end + end +end diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb new file mode 100644 index 00000000000..04f445b4318 --- /dev/null +++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:developer) { create(:user) } + let_it_be(:stranger) { create(:user) } + let_it_be(:old_version) do + create(:design_version, issue: issue, + created_designs: create_list(:design, 3, issue: issue)) + end + let_it_be(:version) do + create(:design_version, issue: issue, + modified_designs: old_version.designs, + created_designs: create_list(:design, 2, issue: issue)) + end + + let(:current_user) { developer } + + def query(vq = version_fields) + graphql_query_for(:project, { fullPath: project.full_path }, + query_graphql_field(:issue, { iid: issue.iid.to_s }, + query_graphql_field(:design_collection, nil, + query_graphql_field(:version, { sha: version.sha }, vq)))) + end + + let(:post_query) { post_graphql(query, current_user: current_user) } + let(:path_prefix) { %w[project issue designCollection version] } + + let(:data) { graphql_data.dig(*path) } + + before do + enable_design_management + project.add_developer(developer) + end + + describe 'scalar fields' do + let(:path) { path_prefix } + let(:version_fields) { query_graphql_field(:sha) } + + before do + post_query + end + + { id: ->(x) { x.to_global_id.to_s }, sha: ->(x) { x.sha } }.each do |field, value| + describe ".#{field}" do + let(:version_fields) { query_graphql_field(field) } + + it "retrieves the #{field}" do + expect(data).to match(a_hash_including(field.to_s => value[version])) + end + end + end + end + + describe 'design_at_version' do + let(:path) { path_prefix + %w[designAtVersion] } + let(:design) { issue.designs.visible_at_version(version).to_a.sample } + let(:design_at_version) { build(:design_at_version, design: design, version: version) } + + let(:version_fields) do + query_graphql_field(:design_at_version, dav_params, 'id filename') + end + + shared_examples :finds_dav do + it 'finds all the designs as of the given version' do + post_query + + expect(data).to match( + a_hash_including( + 'id' => global_id_of(design_at_version), + 'filename' => design.filename + )) + end + + context 'when the current_user is not authorized' do + let(:current_user) { stranger } + + it 'returns nil' do + post_query + + expect(data).to be_nil + end + end + end + + context 'by ID' do + let(:dav_params) { { id: global_id_of(design_at_version) } } + + include_examples :finds_dav + end + + context 'by filename' do + let(:dav_params) { { filename: design.filename } } + + include_examples :finds_dav + end + + context 'by design_id' do + let(:dav_params) { { design_id: global_id_of(design) } } + + include_examples :finds_dav + end + end + + describe 'designs_at_version' do + let(:path) { path_prefix + %w[designsAtVersion edges] } + let(:version_fields) do + query_graphql_field(:designs_at_version, dav_params, 'edges { node { id filename } }') + end + + let(:dav_params) { nil } + + let(:results) do + issue.designs.visible_at_version(version).map do |d| + dav = build(:design_at_version, design: d, version: version) + { 'id' => global_id_of(dav), 'filename' => d.filename } + end + end + + it 'finds all the designs as of the given version' do + post_query + + expect(data.pluck('node')).to match_array(results) + end + + describe 'filtering' do + let(:designs) { issue.designs.sample(3) } + let(:filenames) { designs.map(&:filename) } + let(:ids) do + designs.map { |d| global_id_of(build(:design_at_version, design: d, version: version)) } + end + + before do + post_query + end + + describe 'by filename' do + let(:dav_params) { { filenames: filenames } } + + it 'finds the designs by filename' do + expect(data.map { |e| e.dig('node', 'id') }).to match_array(ids) + end + end + + describe 'by design-id' do + let(:dav_params) { { ids: designs.map { |d| global_id_of(d) } } } + + it 'finds the designs by id' do + expect(data.map { |e| e.dig('node', 'filename') }).to match_array(filenames) + end + end + end + + describe 'pagination' do + let(:end_cursor) { graphql_data_at(*path_prefix, :designs_at_version, :page_info, :end_cursor) } + + let(:ids) do + ::DesignManagement::Design.visible_at_version(version).order(:id).map do |d| + global_id_of(build(:design_at_version, design: d, version: version)) + end + end + + let(:version_fields) do + query_graphql_field(:designs_at_version, { first: 2 }, fields) + end + + let(:cursored_query) do + frag = query_graphql_field(:designs_at_version, { after: end_cursor }, fields) + query(frag) + end + + let(:fields) { ['pageInfo { endCursor }', 'edges { node { id } }'] } + + def response_values(data = graphql_data) + data.dig(*path).map { |e| e.dig('node', 'id') } + end + + it 'sorts designs for reliable pagination' do + post_graphql(query, current_user: current_user) + + expect(response_values).to match_array(ids.take(2)) + + post_graphql(cursored_query, current_user: current_user) + + new_data = Gitlab::Json.parse(response.body).fetch('data') + + expect(response_values(new_data)).to match_array(ids.drop(2)) + end + end + end + + describe 'designs' do + let(:path) { path_prefix + %w[designs edges] } + let(:version_fields) do + query_graphql_field(:designs, nil, 'edges { node { id filename } }') + end + + let(:results) do + version.designs.map do |design| + { 'id' => global_id_of(design), 'filename' => design.filename } + end + end + + it 'finds all the designs as of the given version' do + post_query + + expect(data.pluck('node')).to match_array(results) + end + end +end diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb new file mode 100644 index 00000000000..18787bf925d --- /dev/null +++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Getting versions related to an issue' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:issue) { create(:issue) } + + let_it_be(:version_a) do + create(:design_version, issue: issue) + end + let_it_be(:version_b) do + create(:design_version, issue: issue) + end + let_it_be(:version_c) do + create(:design_version, issue: issue) + end + let_it_be(:version_d) do + create(:design_version, issue: issue) + end + + let_it_be(:owner) { issue.project.owner } + + def version_query(params = version_params) + query_graphql_field(:versions, params, version_query_fields) + end + + let(:version_params) { nil } + + let(:version_query_fields) { ['edges { node { sha } }'] } + + let(:project) { issue.project } + let(:current_user) { owner } + + let(:query) { make_query } + + def make_query(vq = version_query) + graphql_query_for(:project, { fullPath: project.full_path }, + query_graphql_field(:issue, { iid: issue.iid.to_s }, + query_graphql_field(:design_collection, {}, vq))) + end + + let(:design_collection) do + graphql_data_at(:project, :issue, :design_collection) + end + + def response_values(data = graphql_data, key = 'sha') + path = %w[project issue designCollection versions edges] + data.dig(*path).map { |e| e.dig('node', key) } + end + + before do + enable_design_management + end + + it 'returns the design filename' do + post_graphql(query, current_user: current_user) + + expect(response_values).to match_array([version_a, version_b, version_c, version_d].map(&:sha)) + end + + describe 'filter by sha' do + let(:sha) { version_b.sha } + + let(:version_params) { { earlier_or_equal_to_sha: sha } } + + it 'finds only those versions at or before the given cut-off' do + post_graphql(query, current_user: current_user) + + expect(response_values).to contain_exactly(version_a.sha, version_b.sha) + end + end + + describe 'filter by id' do + let(:id) { global_id_of(version_c) } + + let(:version_params) { { earlier_or_equal_to_id: id } } + + it 'finds only those versions at or before the given cut-off' do + post_graphql(query, current_user: current_user) + + expect(response_values).to contain_exactly(version_a.sha, version_b.sha, version_c.sha) + end + end + + describe 'pagination' do + let(:end_cursor) { design_collection.dig('versions', 'pageInfo', 'endCursor') } + + let(:ids) { issue.design_collection.versions.ordered.map(&:sha) } + + let(:query) { make_query(version_query(first: 2)) } + + let(:cursored_query) do + make_query(version_query(after: end_cursor)) + end + + let(:version_query_fields) { ['pageInfo { endCursor }', 'edges { node { sha } }'] } + + it 'sorts designs for reliable pagination' do + post_graphql(query, current_user: current_user) + + expect(response_values).to match_array(ids.take(2)) + + post_graphql(cursored_query, current_user: current_user) + + new_data = Gitlab::Json.parse(response.body).fetch('data') + + expect(response_values(new_data)).to match_array(ids.drop(2)) + end + end +end diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb new file mode 100644 index 00000000000..105ee9daf0c --- /dev/null +++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb @@ -0,0 +1,398 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Getting designs related to an issue' do + include GraphqlHelpers + include DesignManagementTestHelpers + + before_all do + # TODO these tests are being temporarily skipped unless run in EE, + # as we are in the process of moving Design Management to FOSS in 13.0 + # in steps. In the current step the services have not yet been moved, + # which are used by the `:with_smaller_image_versions` factory trait. + # + # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283. + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + end + + let_it_be(:design) { create(:design, :with_smaller_image_versions, versions_count: 1) } + let_it_be(:current_user) { design.project.owner } + let(:design_query) do + <<~NODE + designs { + edges { + node { + id + filename + fullPath + event + image + imageV432x230 + } + } + } + NODE + end + let(:issue) { design.issue } + let(:project) { issue.project } + let(:query) { make_query } + let(:design_collection) do + graphql_data_at(:project, :issue, :design_collection) + end + let(:design_response) do + design_collection.dig('designs', 'edges').first['node'] + end + + def make_query(dq = design_query) + designs_field = query_graphql_field(:design_collection, {}, dq) + issue_field = query_graphql_field(:issue, { iid: issue.iid.to_s }, designs_field) + + graphql_query_for(:project, { fullPath: project.full_path }, issue_field) + end + + def design_image_url(design, ref: nil, size: nil) + Gitlab::UrlBuilder.build(design, ref: ref, size: size) + end + + context 'when the feature is available' do + before do + enable_design_management + end + + it 'returns the design properties correctly' do + version_sha = design.versions.first.sha + + post_graphql(query, current_user: current_user) + + expect(design_response).to eq( + 'id' => design.to_global_id.to_s, + 'event' => 'CREATION', + 'fullPath' => design.full_path, + 'filename' => design.filename, + 'image' => design_image_url(design, ref: version_sha), + 'imageV432x230' => design_image_url(design, ref: version_sha, size: :v432x230) + ) + end + + context 'when the v432x230-sized design image has not been processed' do + before do + allow_next_instance_of(DesignManagement::DesignV432x230Uploader) do |uploader| + allow(uploader).to receive(:file).and_return(nil) + end + end + + it 'returns nil for the v432x230-sized design image' do + post_graphql(query, current_user: current_user) + + expect(design_response['imageV432x230']).to be_nil + end + end + + describe 'pagination' do + before do + create_list(:design, 5, :with_file, issue: issue) + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + let(:issue) { create(:issue) } + + let(:end_cursor) { design_collection.dig('designs', 'pageInfo', 'endCursor') } + + let(:ids) { issue.designs.order(:id).map { |d| global_id_of(d) } } + + let(:query) { make_query(designs_fragment(first: 2)) } + + let(:design_query_fields) { 'pageInfo { endCursor } edges { node { id } }' } + + let(:cursored_query) do + make_query(designs_fragment(after: end_cursor)) + end + + def designs_fragment(params) + query_graphql_field(:designs, params, design_query_fields) + end + + def response_ids(data = graphql_data) + path = %w[project issue designCollection designs edges] + data.dig(*path).map { |e| e.dig('node', 'id') } + end + + it 'sorts designs for reliable pagination' do + expect(response_ids).to match_array(ids.take(2)) + + post_graphql(cursored_query, current_user: current_user) + + new_data = Gitlab::Json.parse(response.body).fetch('data') + + expect(response_ids(new_data)).to match_array(ids.drop(2)) + end + end + + context 'with versions' do + let_it_be(:version) { design.versions.take } + let(:design_query) do + <<~NODE + designs { + edges { + node { + filename + versions { + edges { + node { + id + sha + } + } + } + } + } + } + NODE + end + + it 'includes the version id' do + post_graphql(query, current_user: current_user) + + version_id = design_response['versions']['edges'].first['node']['id'] + + expect(version_id).to eq(version.to_global_id.to_s) + end + + it 'includes the version sha' do + post_graphql(query, current_user: current_user) + + version_sha = design_response['versions']['edges'].first['node']['sha'] + + expect(version_sha).to eq(version.sha) + end + end + + describe 'viewing a design board at a particular version' do + let_it_be(:issue) { design.issue } + let_it_be(:second_design, reload: true) { create(:design, :with_smaller_image_versions, issue: issue, versions_count: 1) } + let_it_be(:deleted_design) { create(:design, :with_versions, issue: issue, deleted: true, versions_count: 1) } + let(:all_versions) { issue.design_versions.ordered.reverse } + let(:design_query) do + <<~NODE + designs(atVersion: "#{version.to_global_id}") { + edges { + node { + id + image + imageV432x230 + event + versions { + edges { + node { + id + } + } + } + } + } + } + NODE + end + let(:design_response) do + design_collection['designs']['edges'] + end + + def global_id(object) + object.to_global_id.to_s + end + + # Filters just design nodes from the larger `design_response` + def design_nodes + design_response.map do |response| + response['node'] + end + end + + # Filters just version nodes from the larger `design_response` + def version_nodes + design_response.map do |response| + response.dig('node', 'versions', 'edges') + end + end + + context 'viewing the original version, when one design was created' do + let(:version) { all_versions.first } + + before do + post_graphql(query, current_user: current_user) + end + + it 'only returns the first design' do + expect(design_nodes).to contain_exactly( + a_hash_including('id' => global_id(design)) + ) + end + + it 'returns the correct full-sized design image' do + expect(design_nodes).to contain_exactly( + a_hash_including('image' => design_image_url(design, ref: version.sha)) + ) + end + + it 'returns the correct v432x230-sized design image' do + expect(design_nodes).to contain_exactly( + a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)) + ) + end + + it 'returns the correct event for the design in this version' do + expect(design_nodes).to contain_exactly( + a_hash_including('event' => 'CREATION') + ) + end + + it 'only returns one version record for the design (the original version)' do + expect(version_nodes).to eq([ + [{ 'node' => { 'id' => global_id(version) } }] + ]) + end + end + + context 'viewing the second version, when one design was created' do + let(:version) { all_versions.second } + + before do + post_graphql(query, current_user: current_user) + end + + it 'only returns the first two designs' do + expect(design_nodes).to contain_exactly( + a_hash_including('id' => global_id(design)), + a_hash_including('id' => global_id(second_design)) + ) + end + + it 'returns the correct full-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('image' => design_image_url(design, ref: version.sha)), + a_hash_including('image' => design_image_url(second_design, ref: version.sha)) + ) + end + + it 'returns the correct v432x230-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)), + a_hash_including('imageV432x230' => design_image_url(second_design, ref: version.sha, size: :v432x230)) + ) + end + + it 'returns the correct events for the designs in this version' do + expect(design_nodes).to contain_exactly( + a_hash_including('event' => 'NONE'), + a_hash_including('event' => 'CREATION') + ) + end + + it 'returns the correct versions records for both designs' do + expect(version_nodes).to eq([ + [{ 'node' => { 'id' => global_id(design.versions.first) } }], + [{ 'node' => { 'id' => global_id(second_design.versions.first) } }] + ]) + end + end + + context 'viewing the last version, when one design was deleted and one was updated' do + let(:version) { all_versions.last } + let!(:second_design_update) do + create(:design_action, :with_image_v432x230, design: second_design, version: version, event: 'modification') + end + + before do + post_graphql(query, current_user: current_user) + end + + it 'does not include the deleted design' do + # The design does exist in the version + expect(version.designs).to include(deleted_design) + + # But the GraphQL API does not include it in these results + expect(design_nodes).to contain_exactly( + a_hash_including('id' => global_id(design)), + a_hash_including('id' => global_id(second_design)) + ) + end + + it 'returns the correct full-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('image' => design_image_url(design, ref: version.sha)), + a_hash_including('image' => design_image_url(second_design, ref: version.sha)) + ) + end + + it 'returns the correct v432x230-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)), + a_hash_including('imageV432x230' => design_image_url(second_design, ref: version.sha, size: :v432x230)) + ) + end + + it 'returns the correct events for the designs in this version' do + expect(design_nodes).to contain_exactly( + a_hash_including('event' => 'NONE'), + a_hash_including('event' => 'MODIFICATION') + ) + end + + it 'returns all versions records for the designs' do + expect(version_nodes).to eq([ + [ + { 'node' => { 'id' => global_id(design.versions.first) } } + ], + [ + { 'node' => { 'id' => global_id(second_design.versions.second) } }, + { 'node' => { 'id' => global_id(second_design.versions.first) } } + ] + ]) + end + end + end + + describe 'a design with note annotations' do + let_it_be(:note) { create(:diff_note_on_design, noteable: design) } + + let(:design_query) do + <<~NODE + designs { + edges { + node { + notesCount + notes { + edges { + node { + id + } + } + } + } + } + } + NODE + end + + let(:design_response) do + design_collection['designs']['edges'].first['node'] + end + + before do + post_graphql(query, current_user: current_user) + end + + it 'returns the notes for the design' do + expect(design_response.dig('notes', 'edges')).to eq( + ['node' => { 'id' => note.to_global_id.to_s }] + ) + end + + it 'returns a note_count for the design' do + expect(design_response['notesCount']).to eq(1) + end + end + end +end diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb new file mode 100644 index 00000000000..0207bb9123a --- /dev/null +++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Getting designs related to an issue' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:design) { create(:design, :with_file, versions_count: 1, issue: issue) } + let_it_be(:current_user) { project.owner } + let_it_be(:note) { create(:diff_note_on_design, noteable: design, project: project) } + + before do + enable_design_management + + note + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + it 'is not too deep for anonymous users' do + note_fields = <<~FIELDS + id + author { name } + FIELDS + + post_graphql(query(note_fields), current_user: nil) + + designs_data = graphql_data['project']['issue']['designs']['designs'] + design_data = designs_data['edges'].first['node'] + note_data = design_data['notes']['edges'].first['node'] + + expect(note_data['id']).to eq(note.to_global_id.to_s) + end + + def query(note_fields = all_graphql_fields_for(Note)) + design_node = <<~NODE + designs { + edges { + node { + notes { + edges { + node { + #{note_fields} + } + } + } + } + } + } + NODE + graphql_query_for( + 'project', + { 'fullPath' => design.project.full_path }, + query_graphql_field( + 'issue', + { iid: design.issue.iid.to_s }, + query_graphql_field( + 'designs', {}, design_node + ) + ) + ) + end +end diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb new file mode 100644 index 00000000000..92d2f9d0d31 --- /dev/null +++ b/spec/requests/api/graphql/project/issue_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Query.project(fullPath).issue(iid)' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:issue_b) { create(:issue, project: project) } + let_it_be(:developer) { create(:user) } + let(:current_user) { developer } + + let_it_be(:project_params) { { 'fullPath' => project.full_path } } + let_it_be(:issue_params) { { 'iid' => issue.iid.to_s } } + let_it_be(:issue_fields) { 'title' } + + let(:query) do + graphql_query_for('project', project_params, project_fields) + end + + let(:project_fields) do + query_graphql_field(:issue, issue_params, issue_fields) + end + + shared_examples 'being able to fetch a design-like object by ID' do + let(:design) { design_a } + let(:path) { %w[project issue designCollection] + [GraphqlHelpers.fieldnamerize(object_field_name)] } + + let(:design_fields) do + [ + query_graphql_field(:filename), + query_graphql_field(:project, nil, query_graphql_field(:id)) + ] + end + + let(:design_collection_fields) do + query_graphql_field(object_field_name, object_params, object_fields) + end + + let(:object_fields) { design_fields } + + context 'the ID is passed' do + let(:object_params) { { id: global_id_of(object) } } + let(:result_fields) { {} } + + let(:expected_fields) do + result_fields.merge({ 'filename' => design.filename, 'project' => id_hash(project) }) + end + + it 'retrieves the object' do + post_query + + data = graphql_data.dig(*path) + + expect(data).to match(a_hash_including(expected_fields)) + end + + context 'the user is unauthorized' do + let(:current_user) { create(:user) } + + it_behaves_like 'a failure to find anything' + end + end + + context 'without parameters' do + let(:object_params) { nil } + + it 'raises an error' do + post_query + + expect(graphql_errors).to include(no_argument_error) + end + end + + context 'attempting to retrieve an object from a different issue' do + let(:object_params) { { id: global_id_of(object_on_other_issue) } } + + it_behaves_like 'a failure to find anything' + end + end + + before do + project.add_developer(developer) + end + + let(:post_query) { post_graphql(query, current_user: current_user) } + + describe '.designCollection' do + include DesignManagementTestHelpers + + let_it_be(:design_a) { create(:design, issue: issue) } + let_it_be(:version_a) { create(:design_version, issue: issue, created_designs: [design_a]) } + + let(:issue_fields) do + query_graphql_field(:design_collection, dc_params, design_collection_fields) + end + + let(:dc_params) { nil } + let(:design_collection_fields) { nil } + + before do + enable_design_management + end + + describe '.design' do + let(:object) { design } + let(:object_field_name) { :design } + + let(:no_argument_error) do + custom_graphql_error(path, a_string_matching(%r/id or filename/)) + end + + let_it_be(:object_on_other_issue) { create(:design, issue: issue_b) } + + it_behaves_like 'being able to fetch a design-like object by ID' + + it_behaves_like 'being able to fetch a design-like object by ID' do + let(:object_params) { { filename: design.filename } } + end + end + + describe '.version' do + let(:version) { version_a } + let(:path) { %w[project issue designCollection version] } + + let(:design_collection_fields) do + query_graphql_field(:version, version_params, 'id sha') + end + + context 'no parameters' do + let(:version_params) { nil } + + it 'raises an error' do + post_query + + expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r/id or sha/))) + end + end + + shared_examples 'a successful query for a version' do + it 'finds the version' do + post_query + + data = graphql_data.dig(*path) + + expect(data).to match( + a_hash_including('id' => global_id_of(version), + 'sha' => version.sha) + ) + end + end + + context '(sha: STRING_TYPE)' do + let(:version_params) { { sha: version.sha } } + + it_behaves_like 'a successful query for a version' + end + + context '(id: ID_TYPE)' do + let(:version_params) { { id: global_id_of(version) } } + + it_behaves_like 'a successful query for a version' + end + end + + describe '.designAtVersion' do + it_behaves_like 'being able to fetch a design-like object by ID' do + let(:object) { build(:design_at_version, design: design, version: version) } + let(:object_field_name) { :design_at_version } + + let(:version) { version_a } + + let(:result_fields) { { 'version' => id_hash(version) } } + let(:object_fields) do + design_fields + [query_graphql_field(:version, nil, query_graphql_field(:id))] + end + + let(:no_argument_error) { missing_required_argument(path, :id) } + + let(:object_on_other_issue) { build(:design_at_version, issue: issue_b) } + end + end + end + + def id_hash(object) + a_hash_including('id' => global_id_of(object)) + end +end diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb new file mode 100644 index 00000000000..26b4c6eafd7 --- /dev/null +++ b/spec/requests/api/graphql/query_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Query' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:developer) { create(:user) } + let(:current_user) { developer } + + describe '.designManagement' do + include DesignManagementTestHelpers + + let_it_be(:version) { create(:design_version, issue: issue) } + let_it_be(:design) { version.designs.first } + let(:query_result) { graphql_data.dig(*path) } + let(:query) { graphql_query_for(:design_management, nil, dm_fields) } + + before do + enable_design_management + project.add_developer(developer) + post_graphql(query, current_user: current_user) + end + + shared_examples 'a query that needs authorization' do + context 'the current user is not able to read designs' do + let(:current_user) { create(:user) } + + it 'does not retrieve the record' do + expect(query_result).to be_nil + end + + it 'raises an error' do + expect(graphql_errors).to include( + a_hash_including('message' => a_string_matching(%r{you don't have permission})) + ) + end + end + end + + describe '.version' do + let(:path) { %w[designManagement version] } + + let(:dm_fields) do + query_graphql_field(:version, { 'id' => global_id_of(version) }, 'id sha') + end + + it_behaves_like 'a working graphql query' + it_behaves_like 'a query that needs authorization' + + context 'the current user is able to read designs' do + it 'fetches the expected data' do + expect(query_result).to eq('id' => global_id_of(version), 'sha' => version.sha) + end + end + end + + describe '.designAtVersion' do + let_it_be(:design_at_version) do + ::DesignManagement::DesignAtVersion.new(design: design, version: version) + end + + let(:path) { %w[designManagement designAtVersion] } + + let(:dm_fields) do + query_graphql_field(:design_at_version, { 'id' => global_id_of(design_at_version) }, <<~FIELDS) + id + filename + version { id sha } + design { id } + issue { title iid } + project { id fullPath } + FIELDS + end + + it_behaves_like 'a working graphql query' + it_behaves_like 'a query that needs authorization' + + context 'the current user is able to read designs' do + it 'fetches the expected data, including the correct associations' do + expect(query_result).to eq( + 'id' => global_id_of(design_at_version), + 'filename' => design_at_version.design.filename, + 'version' => { 'id' => global_id_of(version), 'sha' => version.sha }, + 'design' => { 'id' => global_id_of(design) }, + 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s }, + 'project' => { 'id' => global_id_of(project), 'fullPath' => project.full_path } + ) + end + end + end + end +end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index ff151e891f4..684f0329909 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -917,6 +917,23 @@ describe API::Internal::Base do expect(json_response['status']).to be_falsy end end + + context 'for design repositories' do + let(:gl_repository) { Gitlab::GlRepository::DESIGN.identifier_for_container(project) } + + it 'does not allow access' do + post(api('/internal/allowed'), + params: { + key_id: key.id, + project: project.full_path, + gl_repository: gl_repository, + secret_token: secret_token, + protocol: 'ssh' + }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end end describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb index 3d86fbfe92d..caeb465080e 100644 --- a/spec/requests/api/project_templates_spec.rb +++ b/spec/requests/api/project_templates_spec.rb @@ -7,16 +7,24 @@ describe API::ProjectTemplates do let_it_be(:private_project) { create(:project, :private) } let_it_be(:developer) { create(:user) } + let(:url_encoded_path) { "#{public_project.namespace.path}%2F#{public_project.path}" } + before do private_project.add_developer(developer) end - describe 'GET /projects/:id/templates/:type' do - it 'accepts project paths with dots' do - get api("/projects/#{public_project.namespace.path}%2F#{public_project.path}/templates/dockerfiles") + shared_examples 'accepts project paths with dots' do + it do + subject expect(response).to have_gitlab_http_status(:ok) end + end + + describe 'GET /projects/:id/templates/:type' do + it_behaves_like 'accepts project paths with dots' do + subject { get api("/projects/#{url_encoded_path}/templates/dockerfiles") } + end it 'returns dockerfiles' do get api("/projects/#{public_project.id}/templates/dockerfiles") @@ -81,6 +89,10 @@ describe API::ProjectTemplates do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/template_list') end + + it_behaves_like 'accepts project paths with dots' do + subject { get api("/projects/#{url_encoded_path}/templates/licenses") } + end end describe 'GET /projects/:id/templates/:type/:key' do @@ -150,6 +162,10 @@ describe API::ProjectTemplates do expect(response).to match_response_schema('public_api/v4/license') end + it_behaves_like 'accepts project paths with dots' do + subject { get api("/projects/#{url_encoded_path}/templates/gitlab_ci_ymls/Android") } + end + shared_examples 'path traversal attempt' do |template_type| it 'rejects invalid filenames' do get api("/projects/#{public_project.id}/templates/#{template_type}/%2e%2e%2fPython%2ea") @@ -179,5 +195,9 @@ describe API::ProjectTemplates do expect(content).to include('Project Placeholder') expect(content).to include("Copyright (C) #{Time.now.year} Fullname Placeholder") end + + it_behaves_like 'accepts project paths with dots' do + subject { get api("/projects/#{url_encoded_path}/templates/licenses/mit") } + end end end diff --git a/spec/support/shared_contexts/design_management_shared_contexts.rb b/spec/support/shared_contexts/design_management_shared_contexts.rb new file mode 100644 index 00000000000..2866effb3a8 --- /dev/null +++ b/spec/support/shared_contexts/design_management_shared_contexts.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +shared_context 'four designs in three versions' do + include DesignManagementTestHelpers + + let_it_be(:issue) { create(:issue) } + let_it_be(:project) { issue.project } + let_it_be(:authorized_user) { create(:user) } + + let_it_be(:design_a) { create(:design, issue: issue) } + let_it_be(:design_b) { create(:design, issue: issue) } + let_it_be(:design_c) { create(:design, issue: issue) } + let_it_be(:design_d) { create(:design, issue: issue) } + + let_it_be(:first_version) do + create(:design_version, issue: issue, + created_designs: [design_a], + modified_designs: [], + deleted_designs: []) + end + let_it_be(:second_version) do + create(:design_version, issue: issue, + created_designs: [design_b, design_c, design_d], + modified_designs: [design_a], + deleted_designs: []) + end + let_it_be(:third_version) do + create(:design_version, issue: issue, + created_designs: [], + modified_designs: [design_a], + deleted_designs: [design_d]) + end + + before do + enable_design_management + project.add_developer(authorized_user) + end +end diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb new file mode 100644 index 00000000000..029d7e677da --- /dev/null +++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# To use these shared examples, you may define a value in scope named +# `extra_design_fields`, to pass any extra fields in addition to the +# standard design fields. +RSpec.shared_examples 'a GraphQL type with design fields' do + let(:extra_design_fields) { [] } + + it { expect(described_class).to require_graphql_authorizations(:read_design) } + + it 'exposes the expected design fields' do + expected_fields = %i[ + id + project + issue + filename + full_path + image + image_v432x230 + diff_refs + event + notes_count + ] + extra_design_fields + + expect(described_class).to have_graphql_fields(*expected_fields).only + end + + describe '#image' do + let(:schema) { GitlabSchema } + let(:query) { GraphQL::Query.new(schema) } + let(:context) { double('Context', schema: schema, query: query, parent: nil) } + let(:field) { described_class.fields['image'] } + let(:args) { GraphQL::Query::Arguments::NO_ARGS } + let(:instance) do + object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id)) + object_type.authorized_new(object, query.context) + end + let(:instance_b) do + object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b)) + object_type.authorized_new(object_b, query.context) + end + + it 'resolves to the design image URL' do + image = field.resolve_field(instance, args, context) + sha = design.versions.first.sha + url = ::Gitlab::Routing.url_helpers.project_design_management_designs_raw_image_url(design.project, design, sha) + + expect(image).to eq(url) + end + + it 'has better than O(N) peformance', :request_store do + # Assuming designs have been loaded (as they must be), the following + # queries are required: + # For each distinct version: + # - design_management_versions + # (Request store is needed so that each version is fetched only once.) + # For each distinct issue + # - issues + # For each distinct project + # - projects + # - routes + # - namespaces + # Here that total is: + # - 2 x issues + # - 2 x versions + # - 2 x (projects + routes + namespaces) + # = 10 + expect(instance).not_to eq(instance_b) # preload designs themselves. + expect do + image_a = field.resolve_field(instance, args, context) + image_b = field.resolve_field(instance, args, context) + image_c = field.resolve_field(instance_b, args, context) + image_d = field.resolve_field(instance_b, args, context) + expect(image_a).to eq(image_b) + expect(image_c).not_to eq(image_b) + expect(image_c).to eq(image_d) + end.not_to exceed_query_limit(10) + end + end +end diff --git a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb index aa8979603b6..b0cdc77a378 100644 --- a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb +++ b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb @@ -49,5 +49,29 @@ RSpec.shared_examples 'a valid diff positionable note' do |factory_on_commit| expect(subject.errors).to have_key(:commit_id) end end + + %i(original_position position change_position).each do |method| + describe "#{method}=" do + it "doesn't accept non-hash JSON passed as a string" do + subject.send(:"#{method}=", "true") + expect(subject.attributes_before_type_cast[method.to_s]).to be(nil) + end + + it "does accept a position hash as a string" do + subject.send(:"#{method}=", position.to_json) + expect(subject.position).to eq(position) + end + + it "doesn't accept an array" do + subject.send(:"#{method}=", ["test"]) + expect(subject.attributes_before_type_cast[method.to_s]).to be(nil) + end + + it "does accept a hash" do + subject.send(:"#{method}=", position.to_h) + expect(subject.position).to eq(position) + end + end + end end end |