Testing guidelines
This page provides guidance on how to write policy specs for authorization changes in GitLab.
- Unit tests live in
spec/policies/andee/spec/policies/.
Structure
One describe block per permission
Each permission should have its own describe block. Do not group multiple
permissions into a single block - this makes it harder to identify which
permission a failing test relates to.
# bad - multiple permissions in one block
describe 'read and update issue' do
it { expect_allowed(:read_issue) }
it { expect_allowed(:update_issue) }
end
# good - one block per permission
describe 'read_issue' do
# ...
end
describe 'update_issue' do
# ...
endUse the policy helpers
Use expect_allowed and expect_disallowed, not be_allowed and be_disallowed.
The helpers check each permission separately and report every one that fails.
be_allowed(:a, :b) reports only that the whole set failed.
If one example checks several permissions, pass them to one call:
it 'disallows all permissions' do
expect_disallowed(:read_issue, :update_issue, :delete_issue)
endRole grants
Permissions listed in the role YAML files in config/authz/roles/ are tested automatically.
ee/spec/lib/authz/role_definitions_spec.rb checks that each role gets exactly the permissions
its YAML lists.
Because of this, policy specs do not need role tables, such as a where table of guest through
owner, to test what each role is granted.
Instead, test what the policy adds:
- Prevents, such as licenses, feature flags, settings, visibility, feature access levels, archived projects, and admin mode.
- Grants outside role YAML, such as custom roles and deploy tokens.
Pair each prevent with one allowed example, so the test fails if no one holds the permission.
Test each rule in the policy that defines it
Define a rule as close as possible to the policy that enables the permission. If the rule depends only on that policy’s subject, put it in that policy. If it depends on a more specific subject, put it in the policy that delegates.
For example, GroupPolicy enables update_widget, and WidgetPolicy delegates to GroupPolicy.
A feature flag scoped to the group depends only on the group, so it belongs in GroupPolicy.
A check on the widget itself, such as whether it is locked, belongs in WidgetPolicy.
Test each rule in the spec for the policy that defines it. A policy’s spec should not re-test logic from the policies it delegates to.
Delegating policies
If a policy delegates to another policy, check the delegation with delegate_to
and test only the rules the policy adds.
it { is_expected.to delegate_to(ProjectPolicy) }If the delegate depends on the subject, such as delegate { @subject.container },
check each possible delegate:
it { expect(described_class.new(user, project_trigger)).to delegate_to(ProjectPolicy) }
it { expect(described_class.new(user, group_trigger)).to delegate_to(GroupPolicy) }Create only what the rules need
Most policy specs need one user and one subject. Do not create extra users, projects, or memberships to fill out a role table.
- Use
let_it_befor records. - Reuse the users and projects from the shared policy contexts instead of creating new ones.
Specify the subject in every block
Always define the subject explicitly inside each describe block using
let_it_be. Do not rely on a subject defined at a higher scope - this avoids
accidental test pollution and makes each block self-contained.
# bad - subject defined at top level, shared across all blocks
let_it_be(:project) { create(:project) }
describe 'read_vulnerability' do
# implicitly uses the project above
end
# good - subject defined inside each block
describe 'read_vulnerability' do
let_it_be(:project) { private_project }
where(:current_user, :allowed) do
# ...
end
endUse a descriptively named project fixture that reflects the visibility or
state being tested (private_project, public_project, archived_project),
rather than a generic project.
Full example
RSpec.describe ProjectPolicy do
describe 'write_example_feature' do
let_it_be(:project) { private_project }
let(:current_user) { reporter }
before do
stub_feature_flags(example_feature: true)
stub_licensed_features(example_feature: true)
end
it { expect_allowed(:write_example_feature) }
context 'with admin mode enabled', :enable_admin_mode do
let(:current_user) { admin }
it { expect_allowed(:write_example_feature) }
end
context 'without admin mode enabled' do
let(:current_user) { admin }
it { expect_disallowed(:write_example_feature) }
end
context 'when example_feature feature flag is disabled' do
before do
stub_feature_flags(example_feature: false)
end
it { expect_disallowed(:write_example_feature) }
end
context 'when example_feature licensed feature is disabled' do
before do
stub_licensed_features(example_feature: false)
end
it { expect_disallowed(:write_example_feature) }
end
end
end