3scale/porta

View on GitHub
app/models/cinstance.rb

Summary

Maintainability
D
2 days
Test Coverage
Mass assignment is not restricted using attr_accessible
Class `Cinstance` has 52 methods (exceeds 20 allowed). Consider refactoring.
File `cinstance.rb` has 344 lines of code (exceeds 250 allowed). Consider refactoring.
Cinstance has at least 48 methods
Cinstance has at least 5 instance variables
Cinstance assumes too much for instance variable '@validate_human_edition'
Cinstance assumes too much for instance variable '@validate_plan_is_unique'
class Cinstance < Contract
include SaveDestroyForServiceAssociation
# Maximum number of cinstances permitted between provider and buyer
MAX = 10
 
self.background_deletion = [:referrer_filters, :application_keys, [:alerts, { action: :delete }]]
 
delegate :backend_version, to: :service, allow_nil: true
 
belongs_to :plan, class_name: 'ApplicationPlan', foreign_key: :plan_id, inverse_of: :cinstances
alias application_plan plan
 
# TODO: verify the comment is still true and we can't use inverse_of
belongs_to :service #, inverse_of: :cinstances # this inverse_of messes up some association autosave stuff
 
has_many :alerts, dependent: :delete_all
has_many :line_items
 
# this needs to be before the include Backend::ModelExtensions::Cinstance
# otherwise the application is deleted from backend before the appplication keys are removed, producing errors
with_options(foreign_key: :application_id, dependent: :destroy, inverse_of: :application) do |backend|
backend.has_many :referrer_filters, &::ReferrerFilter::AssociationExtension
backend.has_many :application_keys, &::ApplicationKey::AssociationExtension
end
 
before_validation :set_service_id
before_validation :set_user_key, on: :create
 
before_create :set_service_id
before_create :set_provider_public_key
before_create :accept_on_create, :unless => :live?
 
attr_readonly :service_id
 
include WebHooksHelpers #TODO: make this inclusion more dsl-ish
fires_human_web_hooks_on_events
 
# this has to be before the include Backend::ModelExtensions::Cinstance
# or callbacks order makes keys not to be saved in backend
after_create :create_first_key
 
# before_destroy :refund_fixed_cost
after_commit :reject_if_pending, :on => :destroy
 
include Logic::Contracting::ApplicationContract
 
# FIXME: including Fields after other includes makes Fields break
include Fields::Fields
required_fields_are :name
optional_fields_are :description
default_fields_are :name, :description
set_fields_account_source :user_account
 
include Backend::ModelExtensions::Cinstance
include Finance::VariableCost
include Logic::Authentication::ApplicationContract
include Logic::Keys::ApplicationContract
 
include Indices::AccountIndex::ForDependency
include ThreeScale::Search::Scopes
 
def self.attributes_for_destroy_list
%w( id user_account_id name description user_key plan_id state trial_period_expires_at created_at extra_fields)
end
 
self.allowed_sort_columns = %w{ cinstances.name cinstances.state accounts.org_name cinstances.created_at cinstances.first_daily_traffic_at } # can't order by plans.name, service.name - mysql blows up
self.default_sort_column = :created_at
self.default_sort_direction = :desc
self.allowed_search_scopes = %w{ service_id plan_id plan_type state account account_query state name user_key active_since inactive_since }
self.default_search_scopes = { }
self.sort_columns_joins = {
'accounts.org_name' => [:user_account],
'plans.name' => [:plan],
'service.name' => [:service]
}
 
def redirect_url=(redirect_url)
super(redirect_url.try(:strip))
end
 
validates :conditions,
acceptance: { :message => 'you should agree on the terms and conditions for this plan first' }
 
validates :plan, presence: true
validates :name, presence: { :if => :name_required? }
 
after_commit :push_webhook_key_updated, :on => :update, :if => :user_key_updated?
after_commit :push_application_updated_event, on: :update, unless: :only_traffic_updated?
 
#this method marks that a human edition of the app is happening, thus description presence will be validated
# this is done so e.g. to avoid change_plan to fail when the app misses description or name
Cinstance has missing safe method 'validate_human_edition!'
def validate_human_edition!
@validate_human_edition = true
end
 
Cinstance has missing safe method 'validate_plan_is_unique!'
def validate_plan_is_unique!
@validate_plan_is_unique = true
end
 
def validate_plan_is_unique?
@validate_plan_is_unique
end
 
validate :plan_is_unique, if: :validate_plan_is_unique?
validate :application_id_is_unique, if: :validate_application_id_is_unique?
validates :application_id, uniqueness: { scope: [:service_id], case_sensitive: true }, unless: :validate_application_id_is_unique?
 
validate :user_key_is_unique, unless: :provider_can_duplicate_user_key?
 
validates :user_key, uniqueness: { scope: [:service_id], case_sensitive: true }, if: :provider_can_duplicate_user_key?
 
validate :same_service, on: :update, if: :plan_id_changed?
 
# letter, number, underscore (_), hyphen-minus (-), dot (.), base64 format
# In base64 encoding, the character set is [A-Z,a-z,0-9,and +], if rest length is less than 4, fill of '=' character.
# ^([A-Za-z0-9+]{4})* means the String start with 0 time or more base64 group.
# ([A-Za-z0-9+]{4}|[A-Za-z0-9+]{3}=|[A-Za-z0-9+]{2}==) means the String must end of 3 forms in [A-Za-z0-9+]{4} or [A-Za-z0-9+]{3}= or [A-Za-z0-9+]{2}==
# matches also the non 64B case with (\A[\w\-\.]+\Z)
# NOTE: base64 format also accepts forward slash (/), however we don't allow it because of the restriction on the backend
USER_KEY_FORMAT = /(([\w\-.]+)|([A-Za-z0-9+]{4})*([A-Za-z0-9+]{4}|[A-Za-z0-9+]{3}=|[A-Za-z0-9+]{2}==))/.freeze
 
# The following characters are accepted:
# A-Z a-z 0-9 ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
# Spaces are not allowed
validates :application_id, format: { with: /\A[\x21-\x7E]+\Z/ }, length: { in: 4..255 }
 
validates :user_key, format: { with: /\A#{USER_KEY_FORMAT}\Z/ }, length: { maximum: 256 }
 
scope :order_for_dev_portal, -> { order(service_id: :desc, created_at: :desc) }
 
# Return only live cinstances
#
# Note that deprecated cinstances are still considered live. This is
# intentional, because deprecated cinstances can still be used (so they are
# "live", in a way).
scope :live, -> { where(:state => ['live', 'deprecated'])}
 
scope :active_since, ->(activity_time) {
where.has { first_daily_traffic_at >= activity_time }
}
 
# Return only cinstances live at given time. The time can be single time or
# period (Range object) (from..to).
scope :live_at, ->(period) {
period = period..period unless period.is_a?(Range)
 
where(['cinstances.created_at <= ?', period.end])
}
 
def self.provided_by(account)
# we can access service through plan but also keep service.id in sync with plan.service.id
# this is a simpler way to do the query used historically
joins(:service).where.has { service.sift(:of_account, account) }
end
 
scope :not_bought_by, ->(account) { where.has { user_account_id != account.id } }
 
scope :can_be_managed, -> {
includes(plan: :service)
.where(['services.buyers_manage_apps = ?', true])
.references(:services)
}
scope :latest, -> (count = 5) { reorder(created_at: :desc).limit(count)}
scope :by_user_key, ->(user_key) { where({:user_key => user_key}) }
scope :by_name, ->(query) do
case query.strip
when /\Auser_key:\s*#{Cinstance::USER_KEY_FORMAT}\z/ then by_user_key($1)
else all.merge(Contract.by_name(query))
end
end
scope :by_application_id, ->(app_id) { where({:application_id => app_id}) }
 
def self.by_service(service)
if service == :all || service.blank?
all
else
where { plan_id.in( my { Plan.issued_by(service).select(:id)} ) }
end
end
 
scope :by_service_id, ->(service_id) {
where(service_id: service_id)
}
 
scope :by_active_since, ->(date) { where('first_daily_traffic_at >= ?', date) }
scope :by_inactive_since, ->(date) { where('first_daily_traffic_at <= ?', date) }
 
scope :by_plan_system_name, ->(system_names) {
names = [system_names].flatten
 
proc_like_system_name = proc { |cinstance, name| cinstance.plan.system_name.like(name) }
proc_query_chain = proc { |cinstance| names.map { |name| proc_like_system_name.call(cinstance, name)}.inject(:or) }
 
joins(:plan).where.has { |cinstance| proc_query_chain.call(cinstance) }
}
 
##
# Instance Methods
# and other stuff :(
##
 
# maybe move both limit methods to their models?
 
def self.serialization_preloading(format = nil)
# With Rails 6.1 trying to include plan->issuer without service results in
# > Cannot eagerly load the polymorphic association :issuer
# When both have the same sub-includes, cache takes care of the duplicate queries.
service_includes = %i[proxy account]
plan_includes = [{issuer: service_includes}]
Cinstance#self.serialization_preloading is controlled by argument 'format'
if format == "xml"
service_includes << :default_application_plan
plan_includes << :original
end
includes(:user_account, service: service_includes, plan: plan_includes)
end
 
 
def keys_limit
case service.try(:backend_version)
when 'oauth'
1
else
ApplicationKey::KEYS_LIMIT
end
end
 
def filters_limit
ReferrerFilter::REFERRER_FILTERS_LIMIT
end
 
def buyer_alerts_enabled?
service && service.notify_alerts?(:buyer, :web)
end
 
def period
created_at..Time.zone.now
end
 
def display_name
name.present? ? name : default_name
end
 
def default_name
"Application on plan #{plan.name}"
end
 
delegate :provided_by?, to: :plan
 
# Time value. paid until that Exact time
def variable_cost_paid_until
self[:variable_cost_paid_until] || trial_period_expires_at || created_at
end
 
# Shortcut for plan.service.metrics
def metrics
service && service.all_metrics
end
 
# Is this cinstance bought by an account?
def bought_by?(account)
buyer == account
end
 
Cinstance has missing safe method 'change_provider_public_key!'
def change_provider_public_key!
update_attribute(:provider_public_key, generate_key)
end
 
Cinstance has missing safe method 'change_user_key!'
def change_user_key!
@webhook_event = 'user_key_updated'
update_attribute(:user_key, generate_key)
end
 
def user_key_updated?
saved_change_to_user_key?
end
 
def push_webhook_key_updated
#Push only if updated by User
Cinstance#push_webhook_key_updated calls 'User.current' 2 times
self.web_hook_event!({user: User.current, event: "key_updated"}) if User.current
end
 
def push_application_updated_event
Applications::ApplicationUpdatedEvent.create_and_publish!(self)
end
 
# Reson why cinstance was rejected. This is only set after +reject!+ is called
attr_reader :rejection_reason
 
# Reject pending cinstance.
#
# Note that this is just convenience method equivalent to:
# cinstance.rejection_reason = "i don't like your mother"
# cinstance.destroy
Cinstance has missing safe method 'reject!'
def reject!(reason)
@rejection_reason = reason
destroy
end
 
def select_users
Cinstance#select_users refers to 'c' more than self (maybe move it to another class?)
Cinstance#select_users has the variable name 'c'
service.cinstances.collect {|c| [ c.user_name, c.id ] }
end
 
def available_application_plans
plans_table = Plan.table_name
stock_not_mine = ["(#{plans_table}.original_id = 0 OR
#{plans_table}.original_id IS NULL) AND
#{plans_table}.id <> ?", plan_id]
 
service.application_plans.where(stock_not_mine)
end
 
# Get a usage status object for this cinstance. This object contains information about
# how close this cinstance is to it's usage limits. See ServiceTransaction::Status for
# more details.
def usage_status(options = {})
Backend::Transaction.usage_status(provider_account, self, service, options)
end
 
Method `to_xml` has a Cognitive Complexity of 28 (exceeds 5 allowed). Consider refactoring.
Method `to_xml` has 45 lines of code (exceeds 25 allowed). Consider refactoring.
Cinstance#to_xml has approx 25 statements
def to_xml(options = {})
result = options[:builder] || ThreeScale::XML::Builder.new
 
result.application do |xml|
Cinstance tests 'new_record?' at least 4 times
unless new_record?
xml.id_ id
xml.created_at created_at.xmlschema
xml.updated_at updated_at.xmlschema
end
xml.state state
 
xml.user_account_id user_account_id
xml.first_traffic_at first_traffic_at.try(:xmlschema)
xml.first_daily_traffic_at first_daily_traffic_at.try(:xmlschema)
xml.service_id service.id if service.present?
Cinstance#to_xml calls 'service.backend_version' 2 times
if service.backend_version.v1?
xml.user_key( user_key )
xml.provider_verification_key( provider_public_key )
 
else #v2, oauth on enterprise
xml.application_id( application_id )
 
if service.backend_version.oauth?
xml.redirect_url redirect_url
end
 
unless destroyed?
xml.keys do |keys_element|
Cinstance#to_xml contains iterators nested 3 deep
Cinstance#to_xml has the variable name 'k'
keys.each do |k|
keys_element.key k
end
end
end
end
 
plan.to_xml(:builder => xml)
 
service.oidc_configuration.to_xml(builder: xml) if service.oidc?
 
unless destroyed?
fields_to_xml(xml)
extra_fields_to_xml(xml)
 
if persisted?
if referrer_filters_required?
xml.referrer_filters do |referer_filters_element|
referrers.each do |rf|
referer_filters_element.referrer_filter(rf)
end
end
end
end
end
end
 
result.to_xml
end
 
delegate :custom_keys_enabled?, :referrer_filters_required?, :to => :service
 
def reload(*)
super
ensure
@backend_object = nil
@validate_human_edition = nil
end
 
def backend_object
@backend_object ||= provider_account.backend_object.application(self)
end
 
def create_origin
origin = self[:create_origin]
ActiveSupport::StringInquirer.new(origin.to_s)
end
 
def keys
application_keys.pluck_values
end
 
def referrers
referrer_filters.pluck_values
end
 
def app_plan_change_should_request_credit_card?
service.plan_change_permission(ApplicationPlan) == :request_credit_card
end
 
protected
 
def account_for_sphinx
user_account_id
end
 
def correct_plan_subclass?
unless self.plan.is_a? ApplicationPlan
errors.add(:plan, 'plan must be an ApplicationPlan')
end
end
 
private
 
def only_traffic_updated?
(saved_changes.keys - %w[first_traffic_at first_daily_traffic_at updated_at]).empty?
end
 
# It calls to `create_key_after_create` to check if it's possible to add
# an application key.
#
# Return false if it isn't possible to create a first key
def create_first_key
create_key_after_create? ? application_keys.add : false
end
 
#
# Overrides Contract protected method to run WebHooks after sucessful plan change
#
def change_plan_internal(new_plan)
super do
yield
 
@webhook_event = 'plan_changed'
end
end
 
def same_service
return if plan.blank? || plan.service == service
errors.add(:plan, :not_allowed)
end
 
def reject_if_pending
notify_observers(:rejected) if pending?
end
 
def name_required?
@validate_human_edition
end
 
def service_intentions_required?
issuer && issuer.intentions_required?
end
 
def multiple_applications_allowed?
provider_account && provider_account.multiple_applications_allowed?
end
 
# Custom validation that assures that there is only one non-deleted cinstance
# per plan and user account.
#
# TODO: maybe i can remove this and use regular validates_uniquenes_of?
# validates_uniqueness_of :plan_id, :scope => [:user_account_id], :unless => :multiple_applications_allowed?, :message => 'is already bought'
#
# SURE! If you get rid of acts_as_paranoid because it has to be non-deleted and it keeps cinstances in this table
 
Method `plan_is_unique` has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring.
def plan_is_unique
if plan && user_account && !multiple_applications_allowed?
# All non-deleted cinstance with the same user_account as this one...
others = plan.cinstances.bought_by(user_account)
 
# ...except this one (if already exists in database).
others = others.without_ids(self.id) unless new_record?
 
Cinstance tests 'others.empty?' at least 3 times
errors.add(:plan_id, 'is already bought') unless others.empty?
end
end
 
Method `application_id_is_unique` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.
def application_id_is_unique
if provider_account
others = provider_account.provided_cinstances.by_application_id(application_id)
others = others.without_ids(self.id) unless new_record?
errors.add(:application_id, :taken) unless others.empty?
end
end
 
def validate_application_id_is_unique?
!provider_account.try!(:provider_can_use?, :duplicate_application_id)
end
 
def provider_can_duplicate_user_key?
provider_account.try!(:provider_can_use?, :duplicate_user_key)
end
 
Method `user_key_is_unique` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.
def user_key_is_unique
if provider_account
others = provider_account.provided_cinstances.by_user_key(user_key)
others = others.without_ids(self.id) unless new_record?
errors.add(:user_key, :taken) unless others.empty?
end
end
 
scope :without_ids, ->(id) { where(["#{table_name}.id <> ?", id]) }
 
def set_user_key
self.user_key ||= generate_key
end
 
def set_provider_public_key
self.provider_public_key ||= generate_key
end
 
def set_service_id
self.service_id ||= plan.try(:issuer_id)
end
 
def generate_key
SecureRandom.hex(16)
end
end