diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2017-01-04 22:25:55 +0800 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2017-01-04 22:25:55 +0800 |
commit | 104bac3d215383b76b058e8f61b90fdfac936341 (patch) | |
tree | 5b0737050878d35e0963272810637e83350ce696 /spec/services | |
parent | 99b556976370bfe0c052d15b6a8f0642256173fd (diff) | |
parent | 034d2e4e749ee09649062d3fb1d26c53021ab4d8 (diff) | |
download | gitlab-ce-104bac3d215383b76b058e8f61b90fdfac936341.tar.gz |
Merge branch 'master' into fix-git-hooks-when-creating-file
* master: (1031 commits)
Add changelog entry for renaming API param [ci skip]
Add missing milestone parameter
Refactor issues filter in API
Fix project hooks params
Gitlab::LDAP::Person uses LDAP attributes configuration
Don't delete files from spec/fixtures
Copy, don't move uploaded avatar files
Minor improvements to changelog docs
Rename logo, apply for Slack too
Fix Gemfile.lock for the octokit update
Fix cross-project references copy to include the project reference
Add logo in public files
Use stable icon for Mattermost integration
rewrite the item.respond_to?(:x?) && item.x? to item.try(:x?)
API: extern_uid is a string
Increases pipeline graph drowdown width in order to prevent strange position on chrome on ubuntu
Removed bottom padding from merge manually from CLI because of repositioning award emoji's
Make haml_lint happy
Improve spec
Add feature tests for Cycle Analytics
...
Diffstat (limited to 'spec/services')
-rw-r--r-- | spec/services/access_token_validation_service_spec.rb | 41 | ||||
-rw-r--r-- | spec/services/ci/create_pipeline_service_spec.rb | 17 | ||||
-rw-r--r-- | spec/services/git_push_service_spec.rb | 23 | ||||
-rw-r--r-- | spec/services/groups/create_service_spec.rb | 31 | ||||
-rw-r--r-- | spec/services/groups/update_service_spec.rb | 51 | ||||
-rw-r--r-- | spec/services/issuable/bulk_update_service_spec.rb | 12 | ||||
-rw-r--r-- | spec/services/issues/create_service_spec.rb | 2 | ||||
-rw-r--r-- | spec/services/issues/update_service_spec.rb | 16 | ||||
-rw-r--r-- | spec/services/merge_requests/create_service_spec.rb | 2 | ||||
-rw-r--r-- | spec/services/merge_requests/merge_request_diff_cache_service_spec.rb | 1 | ||||
-rw-r--r-- | spec/services/merge_requests/update_service_spec.rb | 5 | ||||
-rw-r--r-- | spec/services/notes/slash_commands_service_spec.rb | 2 | ||||
-rw-r--r-- | spec/services/projects/fork_service_spec.rb | 13 | ||||
-rw-r--r-- | spec/services/system_note_service_spec.rb | 2 | ||||
-rw-r--r-- | spec/services/users/refresh_authorized_projects_service_spec.rb | 210 |
15 files changed, 410 insertions, 18 deletions
diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb new file mode 100644 index 00000000000..87f093ee8ce --- /dev/null +++ b/spec/services/access_token_validation_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe AccessTokenValidationService, services: true do + describe ".include_any_scope?" do + it "returns true if the required scope is present in the token's scopes" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token).include_any_scope?([:api])).to be(true) + end + + it "returns true if more than one of the required scopes is present in the token's scopes" do + token = double("token", scopes: [:api, :read_user, :other_scope]) + + expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true) + end + + it "returns true if the list of required scopes is an exact match for the token's scopes" do + token = double("token", scopes: [:api, :read_user, :other_scope]) + + expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + end + + it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + end + + it 'returns true if the list of required scopes is blank' do + token = double("token", scopes: []) + + expect(described_class.new(token).include_any_scope?([])).to be(true) + end + + it "returns false if there are no scopes in common between the required scopes and the token scopes" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false) + end + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 4aadd009f3e..ceaca96e25b 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -210,5 +210,22 @@ describe Ci::CreatePipelineService, services: true do expect(result.manual_actions).not_to be_empty end end + + context 'with environment' do + before do + config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' }) + stub_ci_pipeline_yaml_file(config) + end + + it 'creates the environment' do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/master")).not_to be_nil + end + end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index e7624e70725..2a0f00ce937 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -583,7 +583,7 @@ describe GitPushService, services: true do service.push_commits = [commit] expect(ProjectCacheWorker).to receive(:perform_async). - with(project.id, %i(readme)) + with(project.id, %i(readme), %i(commit_count repository_size)) service.update_caches end @@ -596,7 +596,7 @@ describe GitPushService, services: true do it 'does not flush any conditional caches' do expect(ProjectCacheWorker).to receive(:perform_async). - with(project.id, []). + with(project.id, [], %i(commit_count repository_size)). and_call_original service.update_caches @@ -604,6 +604,25 @@ describe GitPushService, services: true do end end + describe '#process_commit_messages' do + let(:service) do + described_class.new(project, + user, + oldrev: sample_commit.parent_id, + newrev: sample_commit.id, + ref: 'refs/heads/master') + end + + it 'only schedules a limited number of commits' do + allow(service).to receive(:push_commits). + and_return(Array.new(1000, double(:commit, to_hash: {}))) + + expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times + + service.process_commit_messages + end + end + def execute_service(project, user, oldrev, newrev, ref) service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref ) service.execute diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 71a0b8e2a12..14717a7455d 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -1,11 +1,12 @@ require 'spec_helper' -describe Groups::CreateService, services: true do - let!(:user) { create(:user) } +describe Groups::CreateService, '#execute', services: true do + let!(:user) { create(:user) } let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } - describe "execute" do - let!(:service) { described_class.new(user, group_params ) } + describe 'visibility level restrictions' do + let!(:service) { described_class.new(user, group_params) } + subject { service.execute } context "create groups without restricted visibility level" do @@ -14,7 +15,29 @@ describe Groups::CreateService, services: true do context "cannot create group with restricted visibility level" do before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) } + it { is_expected.not_to be_persisted } end end + + describe 'creating subgroup' do + let!(:group) { create(:group) } + let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } + + subject { service.execute } + + context 'as group owner' do + before { group.add_owner(user) } + + it { is_expected.to be_persisted } + end + + context 'as guest' do + it 'does not save group and returns an error' do + is_expected.not_to be_persisted + expect(subject.errors[:parent_id].first).to eq('manage access required to create subgroup') + expect(subject.parent_id).to be_nil + end + end + end end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 9c2331144a0..531180e48a1 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe Groups::UpdateService, services: true do - let!(:user) { create(:user) } - let!(:private_group) { create(:group, :private) } - let!(:internal_group) { create(:group, :internal) } - let!(:public_group) { create(:group, :public) } + let!(:user) { create(:user) } + let!(:private_group) { create(:group, :private) } + let!(:internal_group) { create(:group, :internal) } + let!(:public_group) { create(:group, :public) } describe "#execute" do context "project visibility_level validation" do context "public group with public projects" do - let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) } + let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } before do public_group.add_user(user, Gitlab::Access::MASTER) @@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do end context "internal group with internal project" do - let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) } before do internal_group.add_user(user, Gitlab::Access::MASTER) @@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do end context "unauthorized visibility_level validation" do - let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: 99) } before do internal_group.add_user(user, Gitlab::Access::MASTER) end @@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do expect(internal_group.errors.count).to eq(1) end end + + context 'rename group' do + let!(:service) { described_class.new(internal_group, user, path: 'new_path') } + + before do + internal_group.add_user(user, Gitlab::Access::MASTER) + create(:project, :internal, group: internal_group) + end + + it 'returns true' do + expect(service.execute).to eq(true) + end + + context 'error moving group' do + before do + allow(internal_group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) + end + + it 'does not raise an error' do + expect { service.execute }.not_to raise_error + end + + it 'returns false' do + expect(service.execute).to eq(false) + end + + it 'has the right error' do + service.execute + + expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError') + end + + it "hasn't changed the path" do + expect { service.execute}.not_to change { internal_group.reload.path} + end + end + end end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 5f3020b6525..0475f38fe5e 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -52,7 +52,10 @@ describe Issuable::BulkUpdateService, services: true do context 'when the new assignee ID is a valid user' do it 'succeeds' do - result = bulk_update(issue, assignee_id: create(:user).id) + new_assignee = create(:user) + project.team << [new_assignee, :developer] + + result = bulk_update(issue, assignee_id: new_assignee.id) expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) @@ -60,15 +63,16 @@ describe Issuable::BulkUpdateService, services: true do it 'updates the assignee to the use ID passed' do assignee = create(:user) + project.team << [assignee, :developer] expect { bulk_update(issue, assignee_id: assignee.id) } .to change { issue.reload.assignee }.from(user).to(assignee) end end - context 'when the new assignee ID is -1' do - it 'unassigns the issues' do - expect { bulk_update(issue, assignee_id: -1) } + context "when the new assignee ID is #{IssuableFinder::NONE}" do + it "unassigns the issues" do + expect { bulk_update(issue, assignee_id: IssuableFinder::NONE) } .to change { issue.reload.assignee }.to(nil) end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 8bde61ee336..ac3834c32ff 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -135,6 +135,8 @@ describe Issues::CreateService, services: true do end end + it_behaves_like 'issuable create service' + it_behaves_like 'new issuable record that supports slash commands' context 'for a merge request' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 500d224ff98..d83b09fd32c 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -142,6 +142,17 @@ describe Issues::UpdateService, services: true do update_issue(confidential: true) end + + it 'does not update assignee_id with unauthorized users' do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + update_issue(confidential: true) + non_member = create(:user) + original_assignee = issue.assignee + + update_issue(assignee_id: non_member.id) + + expect(issue.reload.assignee_id).to eq(original_assignee.id) + end end context 'todos' do @@ -376,5 +387,10 @@ describe Issues::UpdateService, services: true do let(:mentionable) { issue } include_examples 'updating mentions', Issues::UpdateService end + + include_examples 'issuable update service' do + let(:open_issuable) { issue } + let(:closed_issuable) { create(:closed_issue, project: project) } + end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index b8142889075..673c0bd6c9c 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -84,6 +84,8 @@ describe MergeRequests::CreateService, services: true do end end + it_behaves_like 'issuable create service' + context 'while saving references to issues that the created merge request closes' do let(:first_issue) { create(:issue, project: project) } let(:second_issue) { create(:issue, project: project) } diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index 05cdbe5287a..35804d41b46 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -11,6 +11,7 @@ describe MergeRequests::MergeRequestDiffCacheService do expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) expect(Rails.cache).to receive(:write).with(cache_key, anything) allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true)) + allow_any_instance_of(Repository).to receive(:diffable?).and_return(true) subject.execute(merge_request) end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 790ef765f3a..88c786947d3 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -320,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do expect(issue_ids).to be_empty end end + + include_examples 'issuable update service' do + let(:open_issuable) { merge_request } + let(:closed_issuable) { create(:closed_merge_request, source_project: project) } + end end end diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb index d1099884a02..960b5cd5e6f 100644 --- a/spec/services/notes/slash_commands_service_spec.rb +++ b/spec/services/notes/slash_commands_service_spec.rb @@ -5,6 +5,8 @@ describe Notes::SlashCommandsService, services: true do let(:project) { create(:empty_project) } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:assignee) { create(:user) } + + before { project.team << [assignee, :master] } end shared_examples 'note on noteable that does not support slash commands' do diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 64d15c0523c..8e614211116 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -5,10 +5,12 @@ describe Projects::ForkService, services: true do before do @from_namespace = create(:namespace) @from_user = create(:user, namespace: @from_namespace ) + avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") @from_project = create(:project, creator_id: @from_user.id, namespace: @from_namespace, star_count: 107, + avatar: avatar, description: 'wow such project') @to_namespace = create(:namespace) @to_user = create(:user, namespace: @to_namespace) @@ -36,6 +38,17 @@ describe Projects::ForkService, services: true do it { expect(to_project.namespace).to eq(@to_user.namespace) } it { expect(to_project.star_count).to be_zero } it { expect(to_project.description).to eq(@from_project.description) } + it { expect(to_project.avatar.file).to be_exists } + + # This test is here because we had a bug where the from-project lost its + # avatar after being forked. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158 + it "after forking the from-project still has its avatar" do + # If we do not fork the project first we cannot detect the bug. + expect(to_project).to be_persisted + + expect(@from_project.avatar.file).to be_exists + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 90b7e62bc6f..0e8adb68721 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -694,7 +694,7 @@ describe SystemNoteService, services: true do describe "existing reference" do before do - message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'" + message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title.chomp}'" allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)]) end diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb new file mode 100644 index 00000000000..1f6919151de --- /dev/null +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -0,0 +1,210 @@ +require 'spec_helper' + +describe Users::RefreshAuthorizedProjectsService do + let(:project) { create(:empty_project) } + let(:user) { project.namespace.owner } + let(:service) { described_class.new(user) } + + def create_authorization(project, user, access_level = Gitlab::Access::MASTER) + ProjectAuthorization. + create!(project: project, user: user, access_level: access_level) + end + + describe '#execute' do + before do + user.project_authorizations.delete_all + end + + it 'updates the authorized projects of the user' do + project2 = create(:empty_project) + to_remove = create_authorization(project2, user) + + expect(service).to receive(:update_with_lease). + with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]]) + + service.execute + end + + it 'sets the access level of a project to the highest available level' do + to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER) + + expect(service).to receive(:update_with_lease). + with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]]) + + service.execute + end + + it 'returns a User' do + expect(service.execute).to be_an_instance_of(User) + end + end + + describe '#update_with_lease', :redis do + it 'refreshes the authorizations using a lease' do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). + and_return('foo') + + expect(Gitlab::ExclusiveLease).to receive(:cancel). + with(an_instance_of(String), 'foo') + + expect(service).to receive(:update_authorizations).with([1], []) + + service.update_with_lease([1]) + end + end + + describe '#update_authorizations' do + context 'when there are no rows to add and remove' do + it 'does not change authorizations' do + expect(user).not_to receive(:remove_project_authorizations) + expect(ProjectAuthorization).not_to receive(:insert_authorizations) + + service.update_authorizations([], []) + end + + context 'when the authorized projects column is not set' do + before do + user.update!(authorized_projects_populated: nil) + end + + it 'populates the authorized projects column' do + service.update_authorizations([], []) + + expect(user.authorized_projects_populated).to eq true + end + end + + context 'when the authorized projects column is set' do + before do + user.update!(authorized_projects_populated: true) + end + + it 'does nothing' do + expect(user).not_to receive(:set_authorized_projects_column) + + service.update_authorizations([], []) + end + end + end + + it 'removes authorizations that should be removed' do + authorization = create_authorization(project, user) + + service.update_authorizations([authorization.id]) + + expect(user.project_authorizations).to be_empty + end + + it 'inserts authorizations that should be added' do + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) + + authorizations = user.project_authorizations + + expect(authorizations.length).to eq(1) + expect(authorizations[0].user_id).to eq(user.id) + expect(authorizations[0].project_id).to eq(project.id) + expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER) + end + + it 'populates the authorized projects column' do + # make sure we start with a nil value no matter what the default in the + # factory may be. + user.update!(authorized_projects_populated: nil) + + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) + + expect(user.authorized_projects_populated).to eq(true) + end + end + + describe '#fresh_access_levels_per_project' do + let(:hash) { service.fresh_access_levels_per_project } + + it 'returns a Hash' do + expect(hash).to be_an_instance_of(Hash) + end + + it 'sets the keys to the project IDs' do + expect(hash.keys).to eq([project.id]) + end + + it 'sets the values to the access levels' do + expect(hash.values).to eq([Gitlab::Access::MASTER]) + end + end + + describe '#current_authorizations_per_project' do + before { create_authorization(project, user) } + + let(:hash) { service.current_authorizations_per_project } + + it 'returns a Hash' do + expect(hash).to be_an_instance_of(Hash) + end + + it 'sets the keys to the project IDs' do + expect(hash.keys).to eq([project.id]) + end + + it 'sets the values to the project authorization rows' do + expect(hash.values).to eq([ProjectAuthorization.first]) + end + end + + describe '#current_authorizations' do + context 'without authorizations' do + it 'returns an empty list' do + expect(service.current_authorizations.empty?).to eq(true) + end + end + + context 'with an authorization' do + before { create_authorization(project, user) } + + let(:row) { service.current_authorizations.take } + + it 'returns the currently authorized projects' do + expect(service.current_authorizations.length).to eq(1) + end + + it 'includes the row ID for every row' do + expect(row.id).to be_a_kind_of(Numeric) + end + + it 'includes the project ID for every row' do + expect(row.project_id).to eq(project.id) + end + + it 'includes the access level for every row' do + expect(row.access_level).to eq(Gitlab::Access::MASTER) + end + end + end + + describe '#fresh_authorizations' do + it 'returns the new authorized projects' do + expect(service.fresh_authorizations.length).to eq(1) + end + + it 'returns the highest access level' do + project.team.add_guest(user) + + rows = service.fresh_authorizations.to_a + + expect(rows.length).to eq(1) + expect(rows.first.access_level).to eq(Gitlab::Access::MASTER) + end + + context 'every returned row' do + let(:row) { service.fresh_authorizations.take } + + it 'includes the project ID' do + expect(row.project_id).to eq(project.id) + end + + it 'includes the access level' do + expect(row.access_level).to eq(Gitlab::Access::MASTER) + end + end + end +end |