2021-01-03 14:25:43 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
RSpec.describe API::Unleash do
|
|
|
|
include FeatureFlagHelpers
|
|
|
|
|
|
|
|
let_it_be(:project, refind: true) { create(:project) }
|
2021-09-30 23:02:18 +05:30
|
|
|
|
2021-01-03 14:25:43 +05:30
|
|
|
let(:project_id) { project.id }
|
|
|
|
let(:params) { }
|
|
|
|
let(:headers) { }
|
|
|
|
|
|
|
|
shared_examples 'authenticated request' do
|
|
|
|
context 'when using instance id' do
|
|
|
|
let(:client) { create(:operations_feature_flags_client, project: project) }
|
|
|
|
let(:params) { { instance_id: client.token } }
|
|
|
|
|
|
|
|
it 'responds with OK' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when repository is disabled' do
|
|
|
|
before do
|
|
|
|
project.project_feature.update!(
|
|
|
|
repository_access_level: ::ProjectFeature::DISABLED,
|
|
|
|
merge_requests_access_level: ::ProjectFeature::DISABLED,
|
|
|
|
builds_access_level: ::ProjectFeature::DISABLED
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'responds with forbidden' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when repository is private' do
|
|
|
|
before do
|
|
|
|
project.project_feature.update!(
|
|
|
|
repository_access_level: ::ProjectFeature::PRIVATE,
|
|
|
|
merge_requests_access_level: ::ProjectFeature::DISABLED,
|
|
|
|
builds_access_level: ::ProjectFeature::DISABLED
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'responds with OK' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when using header' do
|
|
|
|
let(:client) { create(:operations_feature_flags_client, project: project) }
|
|
|
|
let(:headers) { { "UNLEASH-INSTANCEID" => client.token }}
|
|
|
|
|
|
|
|
it 'responds with OK' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when using bogus instance id' do
|
|
|
|
let(:params) { { instance_id: 'token' } }
|
|
|
|
|
|
|
|
it 'responds with unauthorized' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when using not existing project' do
|
|
|
|
let(:project_id) { -5000 }
|
|
|
|
let(:params) { { instance_id: 'token' } }
|
|
|
|
|
|
|
|
it 'responds with unauthorized' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples_for 'support multiple environments' do
|
|
|
|
let!(:client) { create(:operations_feature_flags_client, project: project) }
|
|
|
|
let!(:base_headers) { { "UNLEASH-INSTANCEID" => client.token } }
|
|
|
|
let!(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "test" }) }
|
|
|
|
|
|
|
|
let!(:feature_flag_1) do
|
|
|
|
create(:operations_feature_flag, name: "feature_flag_1", project: project, active: true)
|
|
|
|
end
|
|
|
|
|
|
|
|
let!(:feature_flag_2) do
|
|
|
|
create(:operations_feature_flag, name: "feature_flag_2", project: project, active: false)
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
create_scope(feature_flag_1, 'production', false)
|
|
|
|
create_scope(feature_flag_2, 'review/*', true)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not have N+1 problem' do
|
|
|
|
control_count = ActiveRecord::QueryRecorder.new { get api(features_url), headers: headers }.count
|
|
|
|
|
|
|
|
create(:operations_feature_flag, name: "feature_flag_3", project: project, active: true)
|
|
|
|
|
|
|
|
expect { get api(features_url), headers: headers }.not_to exceed_query_limit(control_count)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when app name is staging' do
|
|
|
|
let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "staging" }) }
|
|
|
|
|
|
|
|
it 'returns correct active values' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
feature_flag_1 = json_response['features'].find { |f| f['name'] == 'feature_flag_1' }
|
|
|
|
feature_flag_2 = json_response['features'].find { |f| f['name'] == 'feature_flag_2' }
|
|
|
|
|
|
|
|
expect(feature_flag_1['enabled']).to eq(true)
|
|
|
|
expect(feature_flag_2['enabled']).to eq(false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when app name is production' do
|
|
|
|
let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "production" }) }
|
|
|
|
|
|
|
|
it 'returns correct active values' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
feature_flag_1 = json_response['features'].find { |f| f['name'] == 'feature_flag_1' }
|
|
|
|
feature_flag_2 = json_response['features'].find { |f| f['name'] == 'feature_flag_2' }
|
|
|
|
|
|
|
|
expect(feature_flag_1['enabled']).to eq(false)
|
|
|
|
expect(feature_flag_2['enabled']).to eq(false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when app name is review/patch-1' do
|
|
|
|
let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "review/patch-1" }) }
|
|
|
|
|
|
|
|
it 'returns correct active values' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
feature_flag_1 = json_response['features'].find { |f| f['name'] == 'feature_flag_1' }
|
|
|
|
feature_flag_2 = json_response['features'].find { |f| f['name'] == 'feature_flag_2' }
|
|
|
|
|
|
|
|
expect(feature_flag_1['enabled']).to eq(true)
|
|
|
|
expect(feature_flag_2['enabled']).to eq(false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when app name is empty' do
|
|
|
|
let(:headers) { base_headers }
|
|
|
|
|
|
|
|
it 'returns empty list' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
expect(json_response['features'].count).to eq(0)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
%w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint|
|
|
|
|
describe "GET #{features_endpoint}" do
|
|
|
|
let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) }
|
|
|
|
let(:client) { create(:operations_feature_flags_client, project: project) }
|
|
|
|
|
|
|
|
subject { get api(features_url), params: params, headers: headers }
|
|
|
|
|
|
|
|
it_behaves_like 'authenticated request'
|
|
|
|
|
|
|
|
context 'with version 1 (legacy) feature flags' do
|
2021-09-04 01:27:46 +05:30
|
|
|
let(:feature_flag) { create(:operations_feature_flag, :legacy_flag, project: project, name: 'feature1', active: true, version: 1) }
|
2021-01-03 14:25:43 +05:30
|
|
|
|
2021-09-04 01:27:46 +05:30
|
|
|
it 'does not return a legacy feature flag' do
|
2021-01-03 14:25:43 +05:30
|
|
|
create(:operations_feature_flag_scope,
|
|
|
|
feature_flag: feature_flag,
|
|
|
|
environment_scope: 'sandbox',
|
|
|
|
active: true,
|
|
|
|
strategies: [{ name: "gradualRolloutUserId",
|
|
|
|
parameters: { groupId: "default", percentage: "50" } }])
|
|
|
|
headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "sandbox" }
|
|
|
|
|
|
|
|
get api(features_url), headers: headers
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2021-09-04 01:27:46 +05:30
|
|
|
expect(json_response['features']).to be_empty
|
2021-01-03 14:25:43 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with version 2 feature flags' do
|
|
|
|
it 'does not return a flag without any strategies' do
|
|
|
|
create(:operations_feature_flag, project: project,
|
|
|
|
name: 'feature1', active: true, version: 2)
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features']).to be_empty
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns a flag with a default strategy' do
|
|
|
|
feature_flag = create(:operations_feature_flag, project: project,
|
|
|
|
name: 'feature1', active: true, version: 2)
|
|
|
|
strategy = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'default', parameters: {})
|
|
|
|
create(:operations_scope, strategy: strategy, environment_scope: 'production')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features']).to eq([{
|
|
|
|
'name' => 'feature1',
|
|
|
|
'enabled' => true,
|
|
|
|
'strategies' => [{
|
|
|
|
'name' => 'default',
|
|
|
|
'parameters' => {}
|
|
|
|
}]
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns a flag with a userWithId strategy' do
|
|
|
|
feature_flag = create(:operations_feature_flag, project: project,
|
|
|
|
name: 'feature1', active: true, version: 2)
|
|
|
|
strategy = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'userWithId', parameters: { userIds: 'user123,user456' })
|
|
|
|
create(:operations_scope, strategy: strategy, environment_scope: 'production')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features']).to eq([{
|
|
|
|
'name' => 'feature1',
|
|
|
|
'enabled' => true,
|
|
|
|
'strategies' => [{
|
|
|
|
'name' => 'userWithId',
|
|
|
|
'parameters' => { 'userIds' => 'user123,user456' }
|
|
|
|
}]
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns a flag with multiple strategies' do
|
|
|
|
feature_flag = create(:operations_feature_flag, project: project,
|
|
|
|
name: 'feature1', active: true, version: 2)
|
|
|
|
strategy_a = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'userWithId', parameters: { userIds: 'user_a,user_b' })
|
|
|
|
strategy_b = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '45' })
|
|
|
|
create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
|
|
|
|
create(:operations_scope, strategy: strategy_b, environment_scope: 'production')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features'].map { |f| f['name'] }.sort).to eq(['feature1'])
|
|
|
|
features_json = json_response['features'].map do |feature|
|
|
|
|
feature.merge(feature.slice('strategies').transform_values { |v| v.sort_by { |s| s['name'] } })
|
|
|
|
end
|
|
|
|
expect(features_json).to eq([{
|
|
|
|
'name' => 'feature1',
|
|
|
|
'enabled' => true,
|
|
|
|
'strategies' => [{
|
|
|
|
'name' => 'gradualRolloutUserId',
|
|
|
|
'parameters' => { 'groupId' => 'default', 'percentage' => '45' }
|
|
|
|
}, {
|
|
|
|
'name' => 'userWithId',
|
|
|
|
'parameters' => { 'userIds' => 'user_a,user_b' }
|
|
|
|
}]
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns only flags matching the environment scope' do
|
|
|
|
feature_flag_a = create(:operations_feature_flag, project: project,
|
|
|
|
name: 'feature1', active: true, version: 2)
|
|
|
|
strategy_a = create(:operations_strategy, feature_flag: feature_flag_a)
|
|
|
|
create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
|
|
|
|
feature_flag_b = create(:operations_feature_flag, project: project,
|
|
|
|
name: 'feature2', active: true, version: 2)
|
|
|
|
strategy_b = create(:operations_strategy, feature_flag: feature_flag_b)
|
|
|
|
create(:operations_scope, strategy: strategy_b, environment_scope: 'staging')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'staging' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features'].map { |f| f['name'] }.sort).to eq(['feature2'])
|
|
|
|
expect(json_response['features']).to eq([{
|
|
|
|
'name' => 'feature2',
|
|
|
|
'enabled' => true,
|
|
|
|
'strategies' => [{
|
|
|
|
'name' => 'default',
|
|
|
|
'parameters' => {}
|
|
|
|
}]
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns only strategies matching the environment scope' do
|
|
|
|
feature_flag = create(:operations_feature_flag, project: project,
|
|
|
|
name: 'feature1', active: true, version: 2)
|
|
|
|
strategy_a = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
|
|
|
|
create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
|
|
|
|
strategy_b = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'default', parameters: {})
|
|
|
|
create(:operations_scope, strategy: strategy_b, environment_scope: 'staging')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features']).to eq([{
|
|
|
|
'name' => 'feature1',
|
|
|
|
'enabled' => true,
|
|
|
|
'strategies' => [{
|
|
|
|
'name' => 'userWithId',
|
|
|
|
'parameters' => { 'userIds' => 'user2,user8,user4' }
|
|
|
|
}]
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns only flags for the given project' do
|
|
|
|
project_b = create(:project)
|
|
|
|
feature_flag_a = create(:operations_feature_flag, project: project, name: 'feature_a', active: true, version: 2)
|
|
|
|
strategy_a = create(:operations_strategy, feature_flag: feature_flag_a)
|
|
|
|
create(:operations_scope, strategy: strategy_a, environment_scope: 'sandbox')
|
|
|
|
feature_flag_b = create(:operations_feature_flag, project: project_b, name: 'feature_b', active: true, version: 2)
|
|
|
|
strategy_b = create(:operations_strategy, feature_flag: feature_flag_b)
|
|
|
|
create(:operations_scope, strategy: strategy_b, environment_scope: 'sandbox')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'sandbox' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features']).to eq([{
|
|
|
|
'name' => 'feature_a',
|
|
|
|
'enabled' => true,
|
|
|
|
'strategies' => [{
|
|
|
|
'name' => 'default',
|
|
|
|
'parameters' => {}
|
|
|
|
}]
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns all strategies with a matching scope' do
|
|
|
|
feature_flag = create(:operations_feature_flag, project: project,
|
|
|
|
name: 'feature1', active: true, version: 2)
|
|
|
|
strategy_a = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
|
|
|
|
create(:operations_scope, strategy: strategy_a, environment_scope: '*')
|
|
|
|
strategy_b = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'default', parameters: {})
|
|
|
|
create(:operations_scope, strategy: strategy_b, environment_scope: 'review/*')
|
|
|
|
strategy_c = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '15' })
|
|
|
|
create(:operations_scope, strategy: strategy_c, environment_scope: 'review/patch-1')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'review/patch-1' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features'].first['strategies'].sort_by { |s| s['name'] }).to eq([{
|
|
|
|
'name' => 'default',
|
|
|
|
'parameters' => {}
|
|
|
|
}, {
|
|
|
|
'name' => 'gradualRolloutUserId',
|
|
|
|
'parameters' => { 'groupId' => 'default', 'percentage' => '15' }
|
|
|
|
}, {
|
|
|
|
'name' => 'userWithId',
|
|
|
|
'parameters' => { 'userIds' => 'user2,user8,user4' }
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns a strategy with more than one matching scope' do
|
|
|
|
feature_flag = create(:operations_feature_flag, project: project,
|
|
|
|
name: 'feature1', active: true, version: 2)
|
|
|
|
strategy = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'default', parameters: {})
|
|
|
|
create(:operations_scope, strategy: strategy, environment_scope: 'production')
|
|
|
|
create(:operations_scope, strategy: strategy, environment_scope: '*')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features']).to eq([{
|
|
|
|
'name' => 'feature1',
|
|
|
|
'enabled' => true,
|
|
|
|
'strategies' => [{
|
|
|
|
'name' => 'default',
|
|
|
|
'parameters' => {}
|
|
|
|
}]
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns a disabled flag with a matching scope' do
|
|
|
|
feature_flag = create(:operations_feature_flag, project: project,
|
|
|
|
name: 'myfeature', active: false, version: 2)
|
|
|
|
strategy = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'default', parameters: {})
|
|
|
|
create(:operations_scope, strategy: strategy, environment_scope: 'production')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features']).to eq([{
|
|
|
|
'name' => 'myfeature',
|
|
|
|
'enabled' => false,
|
|
|
|
'strategies' => [{
|
|
|
|
'name' => 'default',
|
|
|
|
'parameters' => {}
|
|
|
|
}]
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns a userWithId strategy for a gitlabUserList strategy' do
|
|
|
|
feature_flag = create(:operations_feature_flag, :new_version_flag, project: project,
|
|
|
|
name: 'myfeature', active: true)
|
|
|
|
user_list = create(:operations_feature_flag_user_list, project: project,
|
|
|
|
name: 'My List', user_xids: 'user1,user2')
|
|
|
|
strategy = create(:operations_strategy, feature_flag: feature_flag,
|
|
|
|
name: 'gitlabUserList', parameters: {}, user_list: user_list)
|
|
|
|
create(:operations_scope, strategy: strategy, environment_scope: 'production')
|
|
|
|
|
|
|
|
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['features']).to eq([{
|
|
|
|
'name' => 'myfeature',
|
|
|
|
'enabled' => true,
|
|
|
|
'strategies' => [{
|
|
|
|
'name' => 'userWithId',
|
|
|
|
'parameters' => { 'userIds' => 'user1,user2' }
|
|
|
|
}]
|
|
|
|
}])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'POST /feature_flags/unleash/:project_id/client/register' do
|
|
|
|
subject { post api("/feature_flags/unleash/#{project_id}/client/register"), params: params, headers: headers }
|
|
|
|
|
|
|
|
it_behaves_like 'authenticated request'
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'POST /feature_flags/unleash/:project_id/client/metrics' do
|
|
|
|
subject { post api("/feature_flags/unleash/#{project_id}/client/metrics"), params: params, headers: headers }
|
|
|
|
|
|
|
|
it_behaves_like 'authenticated request'
|
|
|
|
end
|
|
|
|
end
|