18F/identity-idp

View on GitHub
lib/action_account.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

require_relative './script_base'

# rubocop:disable Metrics/BlockLength
class ActionAccount
  attr_reader :argv, :stdout, :stderr

  def initialize(argv:, stdout:, stderr:)
    @argv = argv
    @stdout = stdout
    @stderr = stderr
  end

  def script_base
    @script_base ||= ScriptBase.new(
      argv:,
      stdout:,
      stderr:,
      subtask_class: subtask(argv.shift),
      banner: banner,
      reason_arg: true,
    )
  end

  def run
    script_base.run
  end

  def banner
    basename = File.basename($PROGRAM_NAME)
    <<~EOS
      #{basename} [subcommand] [arguments] [options]
        Example usage:

          * #{basename} review-reject uuid1 uuid2

          * #{basename} review-pass uuid1 uuid2

          * #{basename} suspend-user uuid1 uuid2

          * #{basename} reinstate-user uuid1 uuid2

          * #{basename} confirm-suspend-user uuid1 uuid2
        Options:
    EOS
  end

  # @api private
  # A subtask is a class that has a run method, the type signature should look like:
  # +#run(args: Array<String>, config: Config) -> Result+
  # @return [Class,nil]
  def subtask(name)
    {
      'review-reject' => ReviewReject,
      'review-pass' => ReviewPass,
      'suspend-user' => SuspendUser,
      'reinstate-user' => ReinstateUser,
      'confirm-suspend-user' => ConfirmSuspendUser,
    }[name]
  end

  module LogBase
    def log_message(uuid:, log:, reason:, table:, messages:)
      table << [uuid, log, reason]
      messages << '`' + uuid + '`: ' + log
      [table, messages]
    end

    def log_text
      {
        no_pending: 'Error: User does not have a pending fraud review',
        rejected_for_fraud: "User's profile has been deactivated due to fraud rejection.",
        profile_activated: "User's profile has been activated and the user has been emailed.",
        error_activating: "There was an error activating the user's profile. Please try again.",
        past_eligibility: 'User is past the 30 day review eligibility.',
        missing_uuid: 'Error: Could not find user with that UUID',
        user_emailed: 'User has been emailed',
        user_suspended: 'User has been suspended',
        user_reinstated: 'User has been reinstated and the user has been emailed',
        user_already_suspended: 'User has already been suspended',
        user_is_not_suspended: 'User is not suspended',
        user_already_reinstated: 'User has already been reinstated',
      }
    end
  end

  module UserActions
    include LogBase

    def perform_user_action(args:, config:, action:)
      table = []
      messages = []
      uuids = args
      table << %w[uuid status reason]

      users = User.where(uuid: uuids).order(:uuid)
      users.each do |user|
        log_texts = []
        case action
        when :suspend
          if user.suspended?
            log_texts << log_text[:user_already_suspended]
          else
            user.suspend!
            log_texts << log_text[:user_suspended]
          end
        when :reinstate
          if user.suspended?
            user.reinstate!
            log_texts << log_text[:user_reinstated]
          elsif user.reinstated?
            log_texts << (log_text[:user_already_reinstated] + " (at #{user.reinstated_at})")
          else
            log_texts << log_text[:user_is_not_suspended]
          end
        when :confirm_suspend
          if user.suspended?
            user.send_email_to_all_addresses(:suspension_confirmed)
            analytics(user).user_suspension_confirmed
            log_texts << log_text[:user_emailed]
          else
            log_texts << log_text[:user_is_not_suspended]
          end
        else
          raise "unknown subtask=#{action}"
        end

        log_texts.each do |text|
          table, messages = log_message(
            uuid: user.uuid,
            log: text,
            reason: config.reason,
            table:,
            messages:,
          )
        end
      end

      if config.include_missing?
        (uuids - users.map(&:uuid)).each do |missing_uuid|
          table, messages = log_message(
            uuid: missing_uuid,
            log: log_text[:missing_uuid],
            reason: config.reason,
            table:,
            messages:,
          )
        end
      end

      ScriptBase::Result.new(
        subtask: "#{action.to_s.dasherize}-user",
        uuids: users.map(&:uuid),
        messages:,
        table:,
      )
    end

    def analytics(user)
      Analytics.new(
        user: user, request: nil, session: {}, sp: nil,
      )
    end
  end

  class ReviewReject
    include LogBase
    def run(args:, config:)
      uuids = args

      users = User.where(uuid: uuids).order(:uuid)

      table = []
      table << %w[uuid status reason]

      messages = []

      users.each do |user|
        profile_fraud_review_pending_at = nil
        success = false

        log_texts = []

        if !user.fraud_review_pending?
          log_texts << log_text[:no_pending]
        elsif FraudReviewChecker.new(user).fraud_review_eligible?
          profile = user.fraud_review_pending_profile
          profile_fraud_review_pending_at = profile.fraud_review_pending_at
          profile_age_in_seconds = profile.profile_age_in_seconds
          profile.reject_for_fraud(notify_user: true)
          success = true

          log_texts << log_text[:rejected_for_fraud]
        else
          log_texts << log_text[:past_eligibility]
        end

        log_texts.each do |text|
          table, messages = log_message(
            uuid: user.uuid,
            log: text,
            reason: config.reason,
            table:,
            messages:,
          )
        end
      ensure
        if !success
          analytics_error_hash = { message: log_texts.last }
        end

        Analytics.new(
          user: user, request: nil, session: {}, sp: nil,
        ).fraud_review_rejected(
          success:,
          errors: analytics_error_hash,
          exception: nil,
          profile_fraud_review_pending_at: profile_fraud_review_pending_at,
          profile_age_in_seconds: profile_age_in_seconds,
        )
      end

      if config.include_missing?
        (uuids - users.map(&:uuid)).each do |missing_uuid|
          table, messages = log_message(
            uuid: missing_uuid,
            log: log_text[:missing_uuid],
            reason: config.reason,
            table:,
            messages:,
          )
        end
        Analytics.new(
          user: AnonymousUser.new, request: nil, session: {}, sp: nil,
        ).fraud_review_rejected(
          success: false,
          errors: { message: log_text[:missing_uuid] },
          exception: nil,
          profile_fraud_review_pending_at: nil,
          profile_age_in_seconds: nil,
        )
      end

      ScriptBase::Result.new(
        subtask: 'review-reject',
        uuids: users.map(&:uuid),
        messages:,
        table:,
      )
    end
  end

  class ReviewPass
    include LogBase

    def run(args:, config:)
      uuids = args

      users = User.where(uuid: uuids).order(:uuid)

      table = []
      table << %w[uuid status reason]

      messages = []
      users.each do |user|
        profile_fraud_review_pending_at = nil
        success = false

        log_texts = []
        if !user.fraud_review_pending?
          log_texts << log_text[:no_pending]
        elsif FraudReviewChecker.new(user).fraud_review_eligible?
          profile = user.fraud_review_pending_profile
          profile_fraud_review_pending_at = profile.fraud_review_pending_at
          profile_age_in_seconds = profile.profile_age_in_seconds
          profile.activate_after_passing_review
          success = true

          if profile.active?
            UserEventCreator.new(current_user: user).
              create_out_of_band_user_event(:account_verified)
            UserAlerts::AlertUserAboutAccountVerified.call(profile: profile)

            log_texts << log_text[:profile_activated]
          else
            log_texts << log_text[:error_activating]
          end
        else
          log_texts << log_text[:past_eligibility]
        end

        log_texts.each do |text|
          table, messages = log_message(
            uuid: user.uuid,
            log: text,
            reason: config.reason,
            table:,
            messages:,
          )
        end
      ensure
        if !success
          analytics_error_hash = { message: log_texts.last }
        end

        Analytics.new(
          user: user, request: nil, session: {}, sp: nil,
        ).fraud_review_passed(
          success:,
          errors: analytics_error_hash,
          exception: nil,
          profile_fraud_review_pending_at: profile_fraud_review_pending_at,
          profile_age_in_seconds: profile_age_in_seconds,
        )
      end

      if config.include_missing?
        (uuids - users.map(&:uuid)).each do |missing_uuid|
          table, messages = log_message(
            uuid: missing_uuid,
            log: log_text[:missing_uuid],
            reason: config.reason,
            table:,
            messages:,
          )
        end
        Analytics.new(
          user: AnonymousUser.new, request: nil, session: {}, sp: nil,
        ).fraud_review_passed(
          success: false,
          errors: { message: log_text[:missing_uuid] },
          exception: nil,
          profile_fraud_review_pending_at: nil,
          profile_age_in_seconds: nil,
        )
      end

      ScriptBase::Result.new(
        subtask: 'review-pass',
        uuids: users.map(&:uuid),
        messages:,
        table:,
      )
    end
  end

  class SuspendUser
    include UserActions

    def run(args:, config:)
      perform_user_action(
        args:,
        config:,
        action: :suspend,
      )
    end
  end

  class ReinstateUser
    include UserActions

    def run(args:, config:)
      perform_user_action(
        args:,
        config:,
        action: :reinstate,
      )
    end
  end

  class ConfirmSuspendUser
    include UserActions

    def run(args:, config:)
      perform_user_action(
        args:,
        config:,
        action: :confirm_suspend,
      )
    end
  end
end
# rubocop:enable Metrics/BlockLength