From 361def36660a93176d8cb32fb47412dbb1100c2b Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 14 Apr 2023 03:18:32 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/issue_templates/Deprecations.md | 4 +- .../lint/redundant_cop_disable_directive.yml | 1 - .rubocop_todo/style/empty_method.yml | 1 - doc/api/graphql/reference/index.md | 3 + .../project/merge_requests/approvals/index.md | 16 ++- .../graphql/loaders/lazy_relation_loader.rb | 74 +++++++++++ .../loaders/lazy_relation_loader/registry.rb | 75 +++++++++++ .../lazy_relation_loader/relation_proxy.rb | 52 ++++++++ .../lazy_relation_loader/top_n_loader.rb | 83 ++++++++++++ lib/gitlab/graphql/pagination/connections.rb | 4 + locale/gitlab.pot | 18 +-- .../lazy_relation_loader/registry_spec.rb | 24 ++++ .../relation_proxy_spec.rb | 29 +++++ .../loaders/lazy_relation_loader_spec.rb | 123 ++++++++++++++++++ 14 files changed, 485 insertions(+), 22 deletions(-) create mode 100644 lib/gitlab/graphql/loaders/lazy_relation_loader.rb create mode 100644 lib/gitlab/graphql/loaders/lazy_relation_loader/registry.rb create mode 100644 lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy.rb create mode 100644 lib/gitlab/graphql/loaders/lazy_relation_loader/top_n_loader.rb create mode 100644 spec/lib/gitlab/graphql/loaders/lazy_relation_loader/registry_spec.rb create mode 100644 spec/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy_spec.rb create mode 100644 spec/lib/gitlab/graphql/loaders/lazy_relation_loader_spec.rb diff --git a/.gitlab/issue_templates/Deprecations.md b/.gitlab/issue_templates/Deprecations.md index 49f1129fe89..30d01c5f5c4 100644 --- a/.gitlab/issue_templates/Deprecations.md +++ b/.gitlab/issue_templates/Deprecations.md @@ -49,9 +49,9 @@ Which tier is this feature available in? Please add links to the relevant merge requests. - As soon as possible, but no later than the third milestone preceding the major release (for example, given the following release schedule: `14.8, 14.9, 14.10, 15.0` – `14.8` is the third milestone preceding the major release): - - [ ] A [deprecation announcement entry](https://about.gitlab.com/handbook/marketing/blog/release-posts/#creating-a-deprecation-announcement) has been created so the deprecation will appear in release posts and on the [general deprecation page](https://docs.gitlab.com/ee/update/deprecations). + - [ ] A [deprecation announcement entry](https://about.gitlab.com/handbook/marketing/blog/release-posts/#creating-the-announcement) has been created so the deprecation will appear in release posts and on the [general deprecation page](https://docs.gitlab.com/ee/update/deprecations). - [ ] Documentation has been updated to mark the feature as [deprecated](https://docs.gitlab.com/ee/development/documentation/versions.html#deprecations-and-removals). -- [ ] On or before the major milestone: A [removal entry](https://about.gitlab.com/handbook/marketing/blog/release-posts/#removals) has been created so the removal will appear on the [removals by milestones](https://docs.gitlab.com/ee/update/removals) page and be announced in the release post. +- [ ] On or before the major milestone: A [removal entry](https://about.gitlab.com/handbook/marketing/blog/release-posts/#creating-the-announcement-1) has been created so the removal will appear on the [removals by milestones](https://docs.gitlab.com/ee/update/removals) page and be announced in the release post. - On the major milestone: - [ ] The deprecated item has been removed. - [ ] If the removal of the deprecated item is a [breaking change](https://about.gitlab.com/handbook/product/gitlab-the-product/#examples-of-breaking-changes), the merge request is labeled ~"breaking change". diff --git a/.rubocop_todo/lint/redundant_cop_disable_directive.yml b/.rubocop_todo/lint/redundant_cop_disable_directive.yml index 15f263ad968..1ae433adc05 100644 --- a/.rubocop_todo/lint/redundant_cop_disable_directive.yml +++ b/.rubocop_todo/lint/redundant_cop_disable_directive.yml @@ -95,7 +95,6 @@ Lint/RedundantCopDisableDirective: - 'ee/app/controllers/ee/groups/group_members_controller.rb' - 'ee/app/controllers/ee/projects/settings/ci_cd_controller.rb' - 'ee/app/controllers/groups/todos_controller.rb' - - 'ee/app/experiments/cart_abandonment_modal_experiment.rb' - 'ee/app/finders/epics/with_issues_finder.rb' - 'ee/app/finders/geo/file_registry_finder.rb' - 'ee/app/finders/geo/project_registry_finder.rb' diff --git a/.rubocop_todo/style/empty_method.yml b/.rubocop_todo/style/empty_method.yml index 3950f8065c5..6e46ec9a3ca 100644 --- a/.rubocop_todo/style/empty_method.yml +++ b/.rubocop_todo/style/empty_method.yml @@ -101,7 +101,6 @@ Style/EmptyMethod: - 'ee/app/controllers/projects/security/sast_configuration_controller.rb' - 'ee/app/controllers/projects/settings/slacks_controller.rb' - 'ee/app/controllers/subscriptions/groups_controller.rb' - - 'ee/app/experiments/cart_abandonment_modal_experiment.rb' - 'ee/app/models/ee/epic.rb' - 'ee/app/services/feature_flag_issues/destroy_service.rb' - 'ee/db/geo/migrate/20170906174622_remove_duplicates_from_project_registry.rb' diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index e6d88904341..dd3da0251d8 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11330,6 +11330,7 @@ Describes a rule for who can approve merge requests. | Name | Type | Description | | ---- | ---- | ----------- | +| `allowMergeWhenInvalid` | [`Boolean`](#boolean) | Indicates if the rule can be ignored if it is invalid. | | `approvalsRequired` | [`Int`](#int) | Number of required approvals. | | `approved` | [`Boolean`](#boolean) | Indicates if the rule is satisfied. | | `approvedBy` | [`UserCoreConnection`](#usercoreconnection) | List of users defined in the rule that approved the merge request. (see [Connections](#connections)) | @@ -11338,6 +11339,7 @@ Describes a rule for who can approve merge requests. | `eligibleApprovers` | [`[UserCore!]`](#usercore) | List of all users eligible to approve the merge request (defined explicitly and from associated groups). | | `groups` | [`GroupConnection`](#groupconnection) | List of groups added as approvers for the rule. (see [Connections](#connections)) | | `id` | [`GlobalID!`](#globalid) | ID of the rule. | +| `invalid` | [`Boolean`](#boolean) | Indicates if the rule is invalid and cannot be approved. | | `name` | [`String`](#string) | Name of the rule. | | `overridden` | [`Boolean`](#boolean) | Indicates if the rule was overridden for the merge request. | | `section` | [`String`](#string) | Named section of the Code Owners file that the rule applies to. | @@ -13052,6 +13054,7 @@ The deployment of an environment. | Name | Type | Description | | ---- | ---- | ----------- | | `approvalSummary` | [`DeploymentApprovalSummary`](#deploymentapprovalsummary) | Approval summary of the deployment.This field can only be resolved for one deployment in any single request. | +| `approvals` | [`[DeploymentApproval!]`](#deploymentapproval) | Current approvals of the deployment. | | `commit` | [`Commit`](#commit) | Commit details of the deployment. | | `createdAt` | [`Time`](#time) | When the deployment record was created. | | `finishedAt` | [`Time`](#time) | When the deployment finished. | diff --git a/doc/user/project/merge_requests/approvals/index.md b/doc/user/project/merge_requests/approvals/index.md index a4272731407..9988cdcddd0 100644 --- a/doc/user/project/merge_requests/approvals/index.md +++ b/doc/user/project/merge_requests/approvals/index.md @@ -107,14 +107,24 @@ Without the approvals, the work cannot merge. Required approvals enable multiple ## Invalid rules -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/334698) in GitLab 15.1. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/334698) in GitLab 15.1. +> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/389905) in GitLab 15.11 [with a flag](../../../../administration/feature_flags.md) named `invalid_scan_result_policy_prevents_merge`. Disabled by default. -Whenever an approval rule cannot be satisfied, the rule will be displayed as `Invalid`. This applies to the following conditions: +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, +ask an administrator to [enable the feature flag](../../../../administration/feature_flags.md) named `invalid_scan_result_policy_prevents_merge`. +On GitLab.com, this feature is available but can be configured by GitLab.com administrators only. + +Whenever an approval rule cannot be satisfied, the rule is displayed as `(!) Auto approved`. This applies to the following conditions: - The only eligible approver is the author of the merge request. - No eligible approvers (either groups or users) have been assigned to the approval rule. -These rules will be automatically approved to unblock their respective merge requests. +These rules will be automatically approved (fail-open state) to unblock their respective merge requests, +unless they were created through a security policy. + +Invalid approval rules created through a security policy are presented with `(!) Action Required` +and are not automatically approved (fail-closed state), blocking their respective merge requests. ## Related topics diff --git a/lib/gitlab/graphql/loaders/lazy_relation_loader.rb b/lib/gitlab/graphql/loaders/lazy_relation_loader.rb new file mode 100644 index 00000000000..69056e87091 --- /dev/null +++ b/lib/gitlab/graphql/loaders/lazy_relation_loader.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Loaders + class LazyRelationLoader + class << self + attr_accessor :model, :association + + # Automatically register the inheriting + # classes to GitlabSchema as lazy objects. + def inherited(klass) + GitlabSchema.lazy_resolve(klass, :load) + end + end + + def initialize(query_ctx, object, **kwargs) + @query_ctx = query_ctx + @object = object + @kwargs = kwargs + + query_ctx[loader_cache_key] ||= Registry.new(relation(**kwargs)) + query_ctx[loader_cache_key].register(object) + end + + # Returns an instance of `RelationProxy` for the object (parent model). + # The returned object behaves like an Active Record relation to support + # keyset pagination. + def load + case reflection.macro + when :has_many + relation_proxy + when :has_one + relation_proxy.last + else + raise 'Not supported association type!' + end + end + + private + + attr_reader :query_ctx, :object, :kwargs + + delegate :model, :association, to: :"self.class" + + # Implement this one if you want to filter the relation + def relation(**) + base_relation + end + + def loader_cache_key + @loader_cache_key ||= self.class.name.to_s + kwargs.sort.to_s + end + + def base_relation + placeholder_record.association(association).scope + end + + # This will only work for HasMany and HasOne associations for now + def placeholder_record + model.new(reflection.active_record_primary_key => 0) + end + + def reflection + model.reflections[association.to_s] + end + + def relation_proxy + RelationProxy.new(object, query_ctx[loader_cache_key]) + end + end + end + end +end diff --git a/lib/gitlab/graphql/loaders/lazy_relation_loader/registry.rb b/lib/gitlab/graphql/loaders/lazy_relation_loader/registry.rb new file mode 100644 index 00000000000..ab2b2bd4dc2 --- /dev/null +++ b/lib/gitlab/graphql/loaders/lazy_relation_loader/registry.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Loaders + class LazyRelationLoader + class Registry + PrematureQueryExecutionTriggered = Class.new(RuntimeError) + # Following methods are Active Record kicker methods which fire SQL query. + # We can support some of them with TopNLoader but for now restricting their use + # as we don't have a use case. + PROHIBITED_METHODS = ( + ActiveRecord::FinderMethods.instance_methods(false) + + ActiveRecord::Calculations.instance_methods(false) + ).to_set.freeze + + def initialize(relation) + @parents = [] + @relation = relation + @records = [] + @loaded = false + end + + def register(object) + @parents << object + end + + def method_missing(method_name, ...) + raise PrematureQueryExecutionTriggered if PROHIBITED_METHODS.include?(method_name) + + result = relation.public_send(method_name, ...) # rubocop:disable GitlabSecurity/PublicSend + + if result.is_a?(ActiveRecord::Relation) # Spawn methods generate a new relation (e.g. where, limit) + @relation = result + + return self + end + + result + end + + def respond_to_missing?(method_name, include_private = false) + relation.respond_to?(method_name, include_private) + end + + def load + return records if loaded + + @loaded = true + @records = TopNLoader.load(relation, parents) + end + + def for(object) + load.select { |record| record[foreign_key] == object[active_record_primary_key] } + .tap { |records| set_inverse_of(object, records) } + end + + private + + attr_reader :parents, :relation, :records, :loaded + + delegate :proxy_association, to: :relation, private: true + delegate :reflection, to: :proxy_association, private: true + delegate :active_record_primary_key, :foreign_key, to: :reflection, private: true + + def set_inverse_of(object, records) + records.each do |record| + object.association(reflection.name).set_inverse_instance(record) + end + end + end + end + end + end +end diff --git a/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy.rb b/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy.rb new file mode 100644 index 00000000000..bab2a272fb0 --- /dev/null +++ b/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Loaders + class LazyRelationLoader + # Proxies all the method calls to Registry instance. + # The main purpose of having this is that calling load + # on an instance of this class will only return the records + # associated with the main Active Record model. + class RelationProxy + def initialize(object, registry) + @object = object + @registry = registry + end + + def load + registry.for(object) + end + alias_method :to_a, :load + + def last(limit = 1) + result = registry.limit(limit) + .reverse_order! + .for(object) + + return result.first if limit == 1 # This is the Active Record behavior + + result + end + + private + + attr_reader :registry, :object + + # Delegate everything to registry + def method_missing(method_name, ...) + result = registry.public_send(method_name, ...) # rubocop:disable GitlabSecurity/PublicSend + + return self if result == registry + + result + end + + def respond_to_missing?(method_name, include_private = false) + registry.respond_to?(method_name, include_private) + end + end + end + end + end +end diff --git a/lib/gitlab/graphql/loaders/lazy_relation_loader/top_n_loader.rb b/lib/gitlab/graphql/loaders/lazy_relation_loader/top_n_loader.rb new file mode 100644 index 00000000000..6404148832b --- /dev/null +++ b/lib/gitlab/graphql/loaders/lazy_relation_loader/top_n_loader.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +# rubocop:disable CodeReuse/ActiveRecord +module Gitlab + module Graphql + module Loaders + class LazyRelationLoader + # Loads the top-n records for each given parent record. + # For example; if you want to load only 5 confidential issues ordered by + # their updated_at column per project for a list of projects by issuing only a single + # SQL query then this class can help you. + # Note that the limit applies per parent record which means that if you apply limit as 5 + # for 10 projects, this loader will load 50 records in total. + class TopNLoader + def self.load(original_relation, parents) + new(original_relation, parents).load + end + + def initialize(original_relation, parents) + @original_relation = original_relation + @parents = parents + end + + def load + klass.select(klass.arel_table[Arel.star]) + .from(from) + .joins("JOIN LATERAL (#{lateral_relation.to_sql}) AS #{klass.arel_table.name} ON true") + .includes(original_includes) + .preload(original_preload) + .eager_load(original_eager_load) + .load + end + + private + + attr_reader :original_relation, :parents + + delegate :proxy_association, to: :original_relation, private: true + delegate :reflection, to: :proxy_association, private: true + delegate :klass, :foreign_key, :active_record, :active_record_primary_key, + to: :reflection, private: true + + # This only works for HasMany and HasOne. + def lateral_relation + original_relation + .unscope(where: foreign_key) # unscoping the where condition generated for the placeholder_record. + .where(klass.arel_table[foreign_key].eq(active_record.arel_table[active_record_primary_key])) + end + + def from + grouping_arel_node.as("#{active_record.arel_table.name}(#{active_record.primary_key})") + end + + def grouping_arel_node + Arel::Nodes::Grouping.new(id_list_arel_node) + end + + def id_list_arel_node + parent_ids.map { |id| [id] } + .then { |ids| Arel::Nodes::ValuesList.new(ids) } + end + + def parent_ids + parents.pluck(active_record.primary_key) + end + + def original_includes + original_relation.includes_values + end + + def original_preload + original_relation.preload_values + end + + def original_eager_load + original_relation.eager_load_values + end + end + end + end + end +end +# rubocop:enable CodeReuse/ActiveRecord diff --git a/lib/gitlab/graphql/pagination/connections.rb b/lib/gitlab/graphql/pagination/connections.rb index 965c01dd02f..df1231b005f 100644 --- a/lib/gitlab/graphql/pagination/connections.rb +++ b/lib/gitlab/graphql/pagination/connections.rb @@ -13,6 +13,10 @@ module Gitlab ActiveRecord::Relation, Gitlab::Graphql::Pagination::Keyset::Connection) + schema.connections.add( + Gitlab::Graphql::Loaders::LazyRelationLoader::RelationProxy, + Gitlab::Graphql::Pagination::Keyset::Connection) + schema.connections.add( Gitlab::Graphql::ExternallyPaginatedArray, Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fd6492a33c3..f87b9a25f71 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13007,6 +13007,9 @@ msgstr "" msgid "CycleAnalytics|New value stream…" msgstr "" +msgid "CycleAnalytics|No data" +msgstr "" + msgid "CycleAnalytics|Number of tasks" msgstr "" @@ -42810,12 +42813,6 @@ msgstr "" msgid "Subscriptions|Activation date" msgstr "" -msgid "Subscriptions|Chat with sales" -msgstr "" - -msgid "Subscriptions|Close" -msgstr "" - msgid "Subscriptions|End date" msgstr "" @@ -42828,18 +42825,9 @@ msgstr "" msgid "Subscriptions|None" msgstr "" -msgid "Subscriptions|Not ready to buy yet?" -msgstr "" - -msgid "Subscriptions|Start a free trial" -msgstr "" - msgid "Subscriptions|Start date" msgstr "" -msgid "Subscriptions|We understand. Maybe you have some questions for our sales team, or maybe you'd like to try some of the paid features first. What would you like to do?" -msgstr "" - msgid "Subscription|Your subscription for %{strong}%{namespace_name}%{strong_close} has expired and you are now on %{pricing_link_start}the GitLab Free tier%{pricing_link_end}. Don't worry, your data is safe. Get in touch with our support team (%{support_email}). They'll gladly help with your subscription renewal." msgstr "" diff --git a/spec/lib/gitlab/graphql/loaders/lazy_relation_loader/registry_spec.rb b/spec/lib/gitlab/graphql/loaders/lazy_relation_loader/registry_spec.rb new file mode 100644 index 00000000000..265839d1236 --- /dev/null +++ b/spec/lib/gitlab/graphql/loaders/lazy_relation_loader/registry_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Graphql::Loaders::LazyRelationLoader::Registry, feature_category: :vulnerability_management do + describe '#respond_to?' do + let(:relation) { Project.all } + let(:registry) { described_class.new(relation) } + + subject { registry.respond_to?(method_name) } + + context 'when the relation responds to given method' do + let(:method_name) { :sorted_by_updated_asc } + + it { is_expected.to be_truthy } + end + + context 'when the relation does not respond to given method' do + let(:method_name) { :this_method_does_not_exist } + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy_spec.rb b/spec/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy_spec.rb new file mode 100644 index 00000000000..f54fb6e77c5 --- /dev/null +++ b/spec/lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Graphql::Loaders::LazyRelationLoader::RelationProxy, feature_category: :vulnerability_management do + describe '#respond_to?' do + let(:object) { double } + let(:registry) { instance_double(Gitlab::Graphql::Loaders::LazyRelationLoader::Registry) } + let(:relation_proxy) { described_class.new(object, registry) } + + subject { relation_proxy.respond_to?(:foo) } + + before do + allow(registry).to receive(:respond_to?).with(:foo, false).and_return(responds_to?) + end + + context 'when the registry responds to given method' do + let(:responds_to?) { true } + + it { is_expected.to be_truthy } + end + + context 'when the registry does not respond to given method' do + let(:responds_to?) { false } + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/lib/gitlab/graphql/loaders/lazy_relation_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/lazy_relation_loader_spec.rb new file mode 100644 index 00000000000..e56cb68c6cb --- /dev/null +++ b/spec/lib/gitlab/graphql/loaders/lazy_relation_loader_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Graphql::Loaders::LazyRelationLoader, feature_category: :vulnerability_management do + let(:query_context) { {} } + let(:args) { {} } + + let_it_be(:project) { create(:project) } + + let(:loader) { loader_class.new(query_context, project, **args) } + + describe '#load' do + subject(:load_relation) { loader.load } + + context 'when the association is has many' do + let_it_be(:public_issue) { create(:issue, project: project) } + let_it_be(:confidential_issue) { create(:issue, :confidential, project: project) } + + let(:loader_class) do + Class.new(described_class) do + self.model = Project + self.association = :issues + + def relation(public_only: false) + relation = base_relation + relation = relation.public_only if public_only + + relation + end + end + end + + it { is_expected.to be_an_instance_of(described_class::RelationProxy) } + + describe '#relation' do + subject { load_relation.load } + + context 'without arguments' do + it { is_expected.to contain_exactly(public_issue, confidential_issue) } + end + + context 'with arguments' do + let(:args) { { public_only: true } } + + it { is_expected.to contain_exactly(public_issue) } + end + end + + describe 'using the same context for different records' do + let_it_be(:another_project) { create(:project) } + + let(:loader_for_another_project) { loader_class.new(query_context, another_project, **args) } + let(:records_for_another_project) { loader_for_another_project.load.load } + let(:records_for_project) { load_relation.load } + + before do + loader # register the original loader to query context + end + + it 'does not mix associated records' do + expect(records_for_another_project).to be_empty + expect(records_for_project).to contain_exactly(public_issue, confidential_issue) + end + + it 'does not cause N+1 queries' do + expect { records_for_another_project }.not_to exceed_query_limit(1) + end + end + + describe 'using Active Record querying methods' do + subject { load_relation.limit(1).load.count } + + it { is_expected.to be(1) } + end + + describe 'using Active Record finder methods' do + subject { load_relation.last(2) } + + it { is_expected.to contain_exactly(public_issue, confidential_issue) } + end + + describe 'calling a method that returns a non relation object' do + subject { load_relation.limit(1).limit_value } + + it { is_expected.to be(1) } + end + + describe 'calling a prohibited method' do + subject(:count) { load_relation.count } + + it 'raises a `PrematureQueryExecutionTriggered` error' do + expect { count }.to raise_error(described_class::Registry::PrematureQueryExecutionTriggered) + end + end + end + + context 'when the association is has one' do + let!(:project_setting) { create(:project_setting, project: project) } + let(:loader_class) do + Class.new(described_class) do + self.model = Project + self.association = :project_setting + end + end + + it { is_expected.to eq(project_setting) } + end + + context 'when the association is belongs to' do + let(:loader_class) do + Class.new(described_class) do + self.model = Project + self.association = :namespace + end + end + + it 'raises error' do + expect { load_relation }.to raise_error(RuntimeError) + end + end + end +end