diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml index e94cc3c..88b43bf 100644 --- a/.github/workflows/brakeman.yml +++ b/.github/workflows/brakeman.yml @@ -11,7 +11,7 @@ runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 # Will run Brakeman checks on dependencies # https://github.com/marketplace/actions/brakeman-linter diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 9a087ce..8f043ff 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -8,7 +8,7 @@ runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 # Will run ES Lint checks on javascript files # https://github.com/marketplace/actions/run-eslint diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml index a4839f6..dbacba0 100644 --- a/.github/workflows/mysql.yml +++ b/.github/workflows/mysql.yml @@ -13,7 +13,7 @@ steps: # Checkout the repo - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 with: fetch-depth: 1 @@ -77,6 +77,12 @@ - name: 'Determine wkhtmltopdf location' run: echo ::set-env name=WICKED_PDF_PATH::$(echo `bundle exec which wkhtmltopdf`) + # Startup MySQL + - name: 'Start MySQL' + run: sudo systemctl start mysql + + + # Install the JS dependencies - name: 'Yarn Install' run: | diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index c6f5ce1..9fa69ff 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -29,7 +29,7 @@ steps: # Checkout the repo - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 with: fetch-depth: 1 diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index 1bb1d76..1d3d2a7 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -10,7 +10,7 @@ runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 # Extract the Ruby version from the Gemfile.lock # - name: 'Determine Ruby Version' @@ -31,4 +31,4 @@ # additional-gems: 'rubocop-dmp_roadmap' - name: 'Placeholder for Rubocop' - run: echo "Rubocop has been temporarily disabled" \ No newline at end of file + run: echo "Rubocop has been temporarily disabled" diff --git a/Gemfile b/Gemfile index 67e418c..9d269a2 100644 --- a/Gemfile +++ b/Gemfile @@ -185,6 +185,9 @@ gem 'activerecord-session_store' +# ------------------------------------------------- +# UTILITIES +gem 'parallel' # ------------------------------------------------ # ENVIRONMENT SPECIFIC DEPENDENCIES diff --git a/Gemfile.lock b/Gemfile.lock index 99f84ce..8528d85 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -513,6 +513,7 @@ omniauth-orcid omniauth-rails_csrf_protection omniauth-shibboleth + parallel pg (~> 0.19.0) progress_bar puma diff --git a/app/controllers/usage_controller.rb b/app/controllers/usage_controller.rb index 15d0cd3..a469ee9 100644 --- a/app/controllers/usage_controller.rb +++ b/app/controllers/usage_controller.rb @@ -43,6 +43,17 @@ send_data(data_csvified, filename: "totals.csv") end + # GET + def org_statistics + authorize :usage + + data = Org::MonthlyUsageService.call + sep = sep_param + data_csvified = Csvable.from_array_of_hashes(data, true, sep) + + send_data(data_csvified, filename: "totals.csv") + end + # POST /usage_filter # rubocop:disable Metrics/MethodLength def filter diff --git a/app/models/stat_exported_plan.rb b/app/models/stat_exported_plan.rb new file mode 100644 index 0000000..0f11074 --- /dev/null +++ b/app/models/stat_exported_plan.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: stats +# +# id :integer not null, primary key +# count :integer default(0) +# date :date not null +# details :text +# type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# org_id :integer +# + +class StatExportedPlan < Stat + + class << self + + def to_csv(exported_plans) + Stat.to_csv(exported_plans) + end + + end + +end diff --git a/app/models/stat_exported_plan/create_or_update.rb b/app/models/stat_exported_plan/create_or_update.rb new file mode 100644 index 0000000..a88b123 --- /dev/null +++ b/app/models/stat_exported_plan/create_or_update.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class StatExportedPlan + + class CreateOrUpdate + + class << self + + def do(start_date:, end_date:, org:) + count = exported_plans(start_date: start_date, end_date: end_date, org_id: org.id) + attrs = { date: end_date.to_date, count: count, org_id: org.id } + + stat_exported_plan = StatExportedPlan.find_by( + date: attrs[:date], + org_id: attrs[:org_id] + ) + + if stat_exported_plan.present? + stat_exported_plan.update(attrs) + else + StatExportedPlan.create(attrs) + end + end + + private + + def users(org_id) + User.where(users: {org_id: org_id }) + end + + def org_plan_ids(org_id) + Role.joins(:user) + .creator + .merge(users(org_id)) + .pluck(:plan_id) + .uniq + end + + def exported_plans(start_date:, end_date:, org_id:) + ExportedPlan.where(plan_id: org_plan_ids(org_id)) + .where(created_at: start_date..end_date) + .count + end + + end + + end + +end diff --git a/app/models/stat_shared_plan.rb b/app/models/stat_shared_plan.rb new file mode 100644 index 0000000..b0ffe10 --- /dev/null +++ b/app/models/stat_shared_plan.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: stats +# +# id :integer not null, primary key +# count :integer default(0) +# date :date not null +# details :text +# type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# org_id :integer +# + +class StatSharedPlan < Stat + + class << self + + def to_csv(shared_plans) + Stat.to_csv(shared_plans) + end + + end + +end diff --git a/app/models/stat_shared_plan/create_or_update.rb b/app/models/stat_shared_plan/create_or_update.rb new file mode 100644 index 0000000..3022d1e --- /dev/null +++ b/app/models/stat_shared_plan/create_or_update.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class StatSharedPlan + + class CreateOrUpdate + + class << self + + def do(start_date:, end_date:, org:) + count = shared_plans(start_date: start_date, end_date: end_date, org_id: org.id) + attrs = { date: end_date.to_date, count: count, org_id: org.id } + + stat_shared_plan = StatSharedPlan.find_by( + date: attrs[:date], + org_id: attrs[:org_id] + ) + + if stat_shared_plan.present? + stat_shared_plan.update(attrs) + else + StatSharedPlan.create(attrs) + end + end + + private + + def users(org_id) + User.where(users: {org_id: org_id }) + end + + def org_plan_ids(org_id) + Role.joins(:user) + .creator + .merge(users(org_id)) + .pluck(:plan_id) + .uniq + end + + def shared_plans(start_date:, end_date:, org_id:) + Role.not_creator + .where(plan_id: org_plan_ids(org_id)) + .where(created_at: start_date..end_date) + .count + end + + end + + end + +end diff --git a/app/policies/usage_policy.rb b/app/policies/usage_policy.rb index 6c26e3c..88d1d92 100644 --- a/app/policies/usage_policy.rb +++ b/app/policies/usage_policy.rb @@ -21,6 +21,10 @@ end def global_statistics? + @user.can_super_admin? + end + + def org_statistics? @user.can_org_admin? end diff --git a/app/services/org/create_created_plan_service.rb b/app/services/org/create_created_plan_service.rb index c668a77..c1cd442 100644 --- a/app/services/org/create_created_plan_service.rb +++ b/app/services/org/create_created_plan_service.rb @@ -1,15 +1,25 @@ # frozen_string_literal: true +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatCreatedPlan +import StatCreatedPlan::CreateOrUpdate +import Role +import User +import Plan +import Perm +import Template + class Org class CreateCreatedPlanService class << self - def call(org = nil) + def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] - orgs.each do |org| + Parallel.each(orgs, in_threads: threads) do |org| OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| StatCreatedPlan::CreateOrUpdate.do( start_date: start_date, diff --git a/app/services/org/create_exported_plan_service.rb b/app/services/org/create_exported_plan_service.rb new file mode 100644 index 0000000..0d1de4c --- /dev/null +++ b/app/services/org/create_exported_plan_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +#import statements fix Circular dependancy errors +import OrgDateRangeable +import StatExportedPlan +import StatExportedPlan::CreateOrUpdate +import Role +import User +import ExportedPlan + +class Org + + class CreateExportedPlanService + + class << self + + def call(org = nil, threads: 0) + orgs = org.nil? ? Org.all : [org] + + Parallel.each(orgs, in_threads: threads) do |org| + OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| + StatExportedPlan::CreateOrUpdate.do( + start_date: start_date, + end_date: end_date, + org: org + ) + end + end + end + + end + + end + +end diff --git a/app/services/org/create_joined_user_service.rb b/app/services/org/create_joined_user_service.rb index e4842eb..44f8ed3 100644 --- a/app/services/org/create_joined_user_service.rb +++ b/app/services/org/create_joined_user_service.rb @@ -1,14 +1,21 @@ # frozen_string_literal: true +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatJoinedUser +import StatJoinedUser::CreateOrUpdate +import User + class Org class CreateJoinedUserService class << self - def call(org = nil) + def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] - orgs.each do |org| + + Parallel.each(orgs, in_threads: threads) do |org| OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| StatJoinedUser::CreateOrUpdate.do( start_date: start_date, @@ -17,6 +24,7 @@ ) end end + # pp StatJoinedUser.where.not(count: 0) end end diff --git a/app/services/org/create_last_month_created_plan_service.rb b/app/services/org/create_last_month_created_plan_service.rb index 8093862..4acea4a 100644 --- a/app/services/org/create_last_month_created_plan_service.rb +++ b/app/services/org/create_last_month_created_plan_service.rb @@ -1,15 +1,26 @@ # frozen_string_literal: true +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatCreatedPlan +import StatCreatedPlan::CreateOrUpdate +import Role +import User +import Plan +import Perm +import Template + + class Org class CreateLastMonthCreatedPlanService class << self - def call(org = nil) + def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] - orgs.each do |org| + Parallel.each(orgs, in_threads: threads) do |org| months = OrgDateRangeable.split_months_from_creation(org) last = months.last if last.present? diff --git a/app/services/org/create_last_month_exported_plan_service.rb b/app/services/org/create_last_month_exported_plan_service.rb new file mode 100644 index 0000000..50ff09f --- /dev/null +++ b/app/services/org/create_last_month_exported_plan_service.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +#import statements fix Circular dependancy errors +import OrgDateRangeable +import StatExportedPlan +import StatExportedPlan::CreateOrUpdate +import Role +import User +import ExportedPlan + +class Org + + class CreateLastMonthExportedPlanService + + class << self + + def call(org = nil, threads: 0) + orgs = org.nil? ? Org.all : [org] + + Parallel.each(orgs, in_threads: threads) do |org| + months = OrgDateRangeable.split_months_from_creation(org) + last = months.last + if last.present? + StatExportedPlan::CreateOrUpdate.do( + start_date: last[:start_date], + end_date: last[:end_date], + org: org + ) + end + end + end + + end + + end + +end diff --git a/app/services/org/create_last_month_joined_user_service.rb b/app/services/org/create_last_month_joined_user_service.rb index 026dfd8..7b53534 100644 --- a/app/services/org/create_last_month_joined_user_service.rb +++ b/app/services/org/create_last_month_joined_user_service.rb @@ -1,14 +1,21 @@ # frozen_string_literal: true +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatJoinedUser +import StatJoinedUser::CreateOrUpdate +import User + class Org class CreateLastMonthJoinedUserService class << self - def call(org = nil) + def call(org = nil, threads: 0) orgs = org.nil? ? ::Org.all : [org] - orgs.each do |org| + + Parallel.each(orgs, in_threads: threads) do |org| months = OrgDateRangeable.split_months_from_creation(org) last = months.last if last.present? diff --git a/app/services/org/create_last_month_shared_plan_service.rb b/app/services/org/create_last_month_shared_plan_service.rb new file mode 100644 index 0000000..9eba8d6 --- /dev/null +++ b/app/services/org/create_last_month_shared_plan_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatSharedPlan +import StatSharedPlan::CreateOrUpdate +import User +import Role + +class Org + + class CreateLastMonthSharedPlanService + + class << self + + def call(org = nil, threads: 0) + orgs = org.nil? ? Org.all : [org] + + Parallel.each(orgs, in_threads: threads) do |org| + months = OrgDateRangeable.split_months_from_creation(org) + last = months.last + if last.present? + StatSharedPlan::CreateOrUpdate.do( + start_date: last[:start_date], + end_date: last[:end_date], + org: org + ) + end + end + end + + end + + end + +end diff --git a/app/services/org/create_shared_plan_service.rb b/app/services/org/create_shared_plan_service.rb new file mode 100644 index 0000000..b784235 --- /dev/null +++ b/app/services/org/create_shared_plan_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatSharedPlan +import StatSharedPlan::CreateOrUpdate +import User +import Role + +class Org + + class CreateSharedPlanService + + class << self + + def call(org = nil, threads: 0) + orgs = org.nil? ? Org.all : [org] + + Parallel.each(orgs, in_threads: threads) do |org| + OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| + StatSharedPlan::CreateOrUpdate.do( + start_date: start_date, + end_date: end_date, + org: org + ) + end + end + end + + end + + end + +end diff --git a/app/services/org/monthly_usage_service.rb b/app/services/org/monthly_usage_service.rb new file mode 100644 index 0000000..fa406c9 --- /dev/null +++ b/app/services/org/monthly_usage_service.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +class Org + + class MonthlyUsageService + + class << self + + def call + total = build_from_joined_user + build_from_created_plan(total) + build_from_shared_plan(total) + build_from_exported_plan(total) + total.values + end + + private + + def current_user + User.find_by_email 'xsrust@gmail.com' + end + + def build_model(month:, new_plans: 0, new_users: 0, downloads: 0, plans_shared: 0) + { + month: month, + new_plans: new_plans, + new_users: new_users, + downloads: downloads, + plans_shared: plans_shared + } + end + + def reducer_body(acc, rec, key_target) + month = rec.date.strftime("%b-%y") + count = rec.count + + if acc[month].present? + acc[month][key_target] = count + else + args = { month: month } + args[key_target] = count + acc[month] = build_model(args) + end + + acc + end + + def build_from_joined_user(total = {}) + joined_users = Stat::StatJoinedUser.monthly_range(org: current_user.org).order(:date) + joined_users.reduce(total) do |acc, rec| + reducer_body(acc, rec, :new_users) + end + end + + def build_from_created_plan(total = {}) + created_plans = Stat::StatCreatedPlan.monthly_range(org: current_user.org).order(:date) + created_plans.reduce(total) do |acc, rec| + reducer_body(acc, rec, :new_plans) + end + end + + def build_from_shared_plan(total = {}) + shared_plans = Stat::StatSharedPlan.monthly_range(org: current_user.org).order(:date) + shared_plans.reduce(total) do |acc, rec| + reducer_body(acc, rec, :plans_shared) + end + end + + def build_from_exported_plan(total = {}) + exported_plans = Stat::StatExportedPlan.monthly_range(org: current_user.org).order(:date) + exported_plans.reduce(total) do |acc, rec| + reducer_body(acc, rec, :downloads) + end + end + + end + + end + +end diff --git a/app/views/usage/_total_usage.html.erb b/app/views/usage/_total_usage.html.erb index 854bc85..c7814b7 100644 --- a/app/views/usage/_total_usage.html.erb +++ b/app/views/usage/_total_usage.html.erb @@ -2,10 +2,10 @@
-
+

Usage statistics

-
+
-
- <%= link_to usage_global_statistics_path(sep: ","), class: 'stat btn btn-default', role: 'button', target: '_blank' do %> - <%= _('Download global usage') %> +
+ <% if current_user.can_super_admin? %> + <%= link_to usage_global_statistics_path(sep: ","), class: 'stat btn btn-default', role: 'button', target: '_blank' do %> + <%= _('Download global usage') %> + <% end %> + <% end %> +
+
+ <%= link_to usage_org_statistics_path(sep: ","), class: 'stat btn btn-default', role: 'button', target: '_blank' do %> + <%= _('Download Monthly Usage') %> <% end %>
diff --git a/config/database.yml.sample b/config/database.yml.sample index a538d9b..76ec376 100644 --- a/config/database.yml.sample +++ b/config/database.yml.sample @@ -3,7 +3,7 @@ encoding: <%= ENV['DB_ADAPTER'] == "mysql2" ? "utf8mb4" : "" %> username: <%= ENV["DB_ADAPTER"] == "postgresql" ? 'postgres' : '' %> database: roadmap_<%= ENV['RAILS_ENV'] %> - pool: 5 + pool: 16 development: <<: *defaults diff --git a/config/routes.rb b/config/routes.rb index baa9f38..c995684 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -129,6 +129,7 @@ post 'usage_filter', controller: 'usage', action: 'filter' get 'usage_all_plans_by_template', controller: 'usage', action: 'all_plans_by_template' get 'usage_global_statistics', controller: 'usage', action: 'global_statistics' + get 'usage_org_statistics', controller: 'usage', action: 'org_statistics' get 'usage_yearly_users', controller: 'usage', action: 'yearly_users' get 'usage_yearly_plans', controller: 'usage', action: 'yearly_plans' diff --git a/lib/tasks/stat.rake b/lib/tasks/stat.rake index c965c43..78522bc 100644 --- a/lib/tasks/stat.rake +++ b/lib/tasks/stat.rake @@ -4,31 +4,96 @@ task build: :environment do Rake::Task['stat:create:created_plan'].execute Rake::Task['stat:create:joined_user'].execute + Rake::Task['stat:create:shared_plan'].execute + Rake::Task['stat:create:exported_plan'].execute Rake::Task['stat:create_last_month:created_plan'].execute Rake::Task['stat:create_last_month:joined_user'].execute + Rake::Task['stat:create_last_month:shared_plan'].execute + Rake::Task['stat:create_last_month:exported_plan'].execute + end + + task build_parallel: :environment do + tasks = ["stat:create:created_plan", + "stat:create:joined_user", + "stat:create:shared_plan", + "stat:create:exported_plan", + "stat:create_last_month:created_plan", + "stat:create_last_month:joined_user", + "stat:create_last_month:shared_plan", + "stat:create_last_month:exported_plan"] + + Parallel.each(tasks, progress: "Building Stats", in_processes: 4) do |task| + Rake::Task[task].execute + task + end + end + + task build_last_month: :environment do + tasks = ["stat:create_last_month:created_plan", + "stat:create_last_month:joined_user", + "stat:create_last_month:shared_plan", + "stat:create_last_month:exported_plan"] + + tasks.each do |task| + Rake::Task[task].execute + end + end + + task build_last_month_parallel: :environment do + tasks = ["stat:create_last_month:created_plan", + "stat:create_last_month:joined_user", + "stat:create_last_month:shared_plan", + "stat:create_last_month:exported_plan"] + + Parallel.each(tasks) do |task| + Rake::Task[task].execute + task + end end namespace :create do desc "Creates created plan stats for every org since they joined" task created_plan: :environment do - Org::CreateCreatedPlanService.call + Org::CreateCreatedPlanService.call(threads: 2) end desc "Creates joined user stats for every org since they joined" task joined_user: :environment do - Org::CreateJoinedUserService.call + Org::CreateJoinedUserService.call(threads: 2) end + + desc "Creates shared plan stats for every org since they joined" + task shared_plan: :environment do + Org::CreateSharedPlanService.call(threads: 2) + end + + desc "Creates exported plan stats for every org since they joined" + task exported_plan: :environment do + Org::CreateExportedPlanService.call(threads: 2) + end + end namespace :create_last_month do desc "Creates created plan stats for today's last month for every org" task created_plan: :environment do - Org::CreateLastMonthCreatedPlanService.call + Org::CreateLastMonthCreatedPlanService.call(threads: 2) end desc "Creates joined user stats for today's last month for every org" task joined_user: :environment do - Org::CreateLastMonthJoinedUserService.call + Org::CreateLastMonthJoinedUserService.call(threads: 2) + end + + desc "Creates shared plan stats for today's last month for every org" + task shared_plan: :environment do + Org::CreateLastMonthSharedPlanService.call(threads: 2) + end + + desc "created exported plan stats for today's last month for every org" + task exported_plan: :environment do + Org::CreateLastMonthExportedPlanService.call(threads: 2) end end + end diff --git a/spec/controllers/usage_controller_spec.rb b/spec/controllers/usage_controller_spec.rb index a0d07f7..da77804 100644 --- a/spec/controllers/usage_controller_spec.rb +++ b/spec/controllers/usage_controller_spec.rb @@ -10,7 +10,7 @@ @plan_stat = create(:stat_created_plan, date: @date, org: @org, details: @details) @user_stat = create(:stat_joined_user, date: @date, org: @org) - sign_in(create(:user, :org_admin, org: @org)) + sign_in(create(:user, :super_admin, org: @org)) end describe "GET /usage (aka index)" do diff --git a/spec/policies/usage_policy_spec.rb b/spec/policies/usage_policy_spec.rb index 9949510..3097392 100644 --- a/spec/policies/usage_policy_spec.rb +++ b/spec/policies/usage_policy_spec.rb @@ -6,36 +6,44 @@ subject { described_class.new(user, :usage) } - let(:actions) do + let(:super_actions) do %i[index filter global_statistics yearly_users yearly_plans - all_plans_by_template plans_by_template] + all_plans_by_template plans_by_template org_statistics] + end + let(:org_actions) do + %i[index filter yearly_users yearly_plans + all_plans_by_template plans_by_template org_statistics] end context "super_admin" do let(:user) { create(:user, :super_admin) } it "has access to all actions" do - is_expected.to permit_actions(actions) + is_expected.to permit_actions(super_actions) end end context "org_admin" do let(:user) { create(:user, :org_admin) } - it "has access to all actions" do - is_expected.to permit_actions(actions) + it "has access to all org-admin actions" do + is_expected.to permit_actions(org_actions) + end + + it "does not have access to global statistics" do + is_expected.to forbid_actions(%i[global_statistics]) end end context "user" do let(:user) { create(:user) } it "not have access to any of the actions" do - is_expected.to forbid_actions(actions) + is_expected.to forbid_actions(super_actions) end end context "unauthenticated" do let(:user) { nil } it "not have access to any of the actions" do - actions.each do |action| + super_actions.each do |action| # rubocop:disable Layout/LineLength expect { is_expected.to permit_action(action) }.to raise_error(Pundit::NotAuthorizedError), "expected :#{action} to raise a NotAuthorizedError" # rubocop:enable Layout/LineLength