diff --git a/app/controllers/admin/api/accounts_controller.rb b/app/controllers/admin/api/accounts_controller.rb index 173737ba3d..e2422e1ebc 100644 --- a/app/controllers/admin/api/accounts_controller.rb +++ b/app/controllers/admin/api/accounts_controller.rb @@ -6,7 +6,7 @@ class Admin::Api::AccountsController < Admin::Api::BaseController # Account List # GET /admin/api/accounts.xml def index - accounts = buyer_accounts.includes(:users, :settings, :payment_detail, :country, bought_plans: [:original]) # :issuer is polymorphic + accounts = buyer_accounts.includes(:users, :settings, :payment_detail, :country, :annotations, bought_plans: [:original]) # :issuer is polymorphic if state = params[:state].presence accounts = accounts.where(:state => state.to_s) diff --git a/app/controllers/admin/api/backend_apis_controller.rb b/app/controllers/admin/api/backend_apis_controller.rb index f1c10558e7..342db4c2bc 100644 --- a/app/controllers/admin/api/backend_apis_controller.rb +++ b/app/controllers/admin/api/backend_apis_controller.rb @@ -8,7 +8,7 @@ class Admin::Api::BackendApisController < Admin::Api::BaseController clear_respond_to respond_to :json - wrap_parameters BackendApi + wrap_parameters BackendApi, include: BackendApi.attribute_names | %w[annotations] representer BackendApi paginate only: :index @@ -48,7 +48,7 @@ def destroy private - DEFAULT_PARAMS = %i[name description private_endpoint].freeze + DEFAULT_PARAMS = [:name, :description, :private_endpoint, {annotations: {}}].freeze private_constant :DEFAULT_PARAMS def authorize diff --git a/app/controllers/admin/api/services_controller.rb b/app/controllers/admin/api/services_controller.rb index 32da35c212..9b95e87576 100644 --- a/app/controllers/admin/api/services_controller.rb +++ b/app/controllers/admin/api/services_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Admin::Api::ServicesController < Admin::Api::ServiceBaseController - wrap_parameters Service, include: Service.attribute_names | %w[state_event] + wrap_parameters Service, include: Service.attribute_names | %w[state_event annotations] representer Service before_action :deny_on_premises_for_master @@ -12,7 +12,7 @@ class Admin::Api::ServicesController < Admin::Api::ServiceBaseController # Service List # GET /admin/api/services.xml def index - services = accessible_services.includes(:proxy, :account).order(:id).paginate(pagination_params) + services = accessible_services.includes(:proxy, :account, :annotations).order(:id).paginate(pagination_params) respond_with(services) end @@ -62,7 +62,8 @@ def service_params :buyer_can_select_plan, :buyer_plan_change_permission, :buyers_manage_keys, :buyer_key_regenerate_enabled, :mandatory_app_key, :custom_keys_enabled, :state_event, :txt_support, :terms, - {notification_settings: [web_provider: [], email_provider: [], web_buyer: [], email_buyer: []]}] + {notification_settings: [web_provider: [], email_provider: [], web_buyer: [], email_buyer: []], + annotations: {}}] params.require(:service).permit(*permitted_params) end diff --git a/app/controllers/master/api/providers_controller.rb b/app/controllers/master/api/providers_controller.rb index 470e81b060..cad60a29a3 100644 --- a/app/controllers/master/api/providers_controller.rb +++ b/app/controllers/master/api/providers_controller.rb @@ -82,7 +82,7 @@ def plan_upgrade end end - UPDATE_PARAMS = %i[from_email support_email finance_support_email site_access_code state_event].freeze + UPDATE_PARAMS = [:from_email, :support_email, :finance_support_email, :site_access_code, :state_event, {annotations: {}}].freeze private_constant :UPDATE_PARAMS private diff --git a/app/models/account.rb b/app/models/account.rb index 7a91f78fd1..3ae8596eca 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -84,6 +84,7 @@ class Account < ApplicationRecord scope :searchable, -> { not_master.without_to_be_deleted.includes(:users, :bought_cinstances) } + annotated audited # this is done in a callback because we want to do this AFTER the account is deleted @@ -323,7 +324,7 @@ def country=(country_name) end def special_fields - [:country] + %i[country annotations] end # Returns the id corresponding to an account with given api key. This function avoids @@ -483,6 +484,7 @@ def to_xml(options = {}) end xml.state state + annotations_xml(:builder => xml) xml.deletion_date deletion_date.xmlschema if scheduled_for_deletion? && deletion_date if provider? diff --git a/app/models/annotation.rb b/app/models/annotation.rb new file mode 100644 index 0000000000..4f9f74c8d4 --- /dev/null +++ b/app/models/annotation.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Annotation < ApplicationRecord + SUPPORTED_ANNOTATIONS = %w[managed_by].freeze + + belongs_to :annotated, polymorphic: true, optional: false, inverse_of: :annotations + + validates :name, presence: true, inclusion: { in: SUPPORTED_ANNOTATIONS } + validates :value, presence: true + validates :name, :value, :annotated_type, length: { maximum: 255 } +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 907f1c3335..a3bcdee2b3 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -3,6 +3,7 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true + include Annotating include BackgroundDeletion def self.user_attribute_names diff --git a/app/models/backend_api.rb b/app/models/backend_api.rb index add339b7e9..262ff6b67d 100644 --- a/app/models/backend_api.rb +++ b/app/models/backend_api.rb @@ -5,7 +5,8 @@ class BackendApi < ApplicationRecord include SystemName include ProxyConfigAffectingChanges::ModelExtension - audited :allow_mass_assignment => true + annotated + audited define_proxy_config_affecting_attributes :private_endpoint diff --git a/app/models/concerns/annotating.rb b/app/models/concerns/annotating.rb new file mode 100644 index 0000000000..7ed0bf566e --- /dev/null +++ b/app/models/concerns/annotating.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Annotating + extend ActiveSupport::Concern + + class_methods do + def annotated + class_eval do + include Model + include ManagedBy + end + end + end + + class << self + def models + return @models if @models + + # This is to see all models when creating the DB trigger, otherwise the resulting trigger could be incorrect + # https://github.com/3scale/porta/pull/3857#discussion_r1707235658 + Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models") + + @models = ActiveRecord::Base.descendants.select { |model| model.include?(Model) } + end + end +end diff --git a/app/models/concerns/annotating/managed_by.rb b/app/models/concerns/annotating/managed_by.rb new file mode 100644 index 0000000000..70c0e82a6a --- /dev/null +++ b/app/models/concerns/annotating/managed_by.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Annotating + module ManagedBy + extend ActiveSupport::Concern + + def managed_by + value_of_annotation("managed_by") + end + + def managed_by=(value) + annotate("managed_by", value) + end + end +end diff --git a/app/models/concerns/annotating/model.rb b/app/models/concerns/annotating/model.rb new file mode 100644 index 0000000000..5f326e7624 --- /dev/null +++ b/app/models/concerns/annotating/model.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Annotating + module Model + extend ActiveSupport::Concern + + included do + has_many :annotations, as: :annotated, dependent: :destroy, autosave: true, inverse_of: :annotated + end + + def annotations=(hash) + hash.each do |k ,v| + annotate(k, v) + end + end + + def annotations_hash + annotations.pluck(:name, :value).to_h + end + + def annotations_xml(options = {}) + xml = options[:builder] || ThreeScale::XML::Builder.new + + xml.annotations do + annotations.each do |annotation| + xml.tag!(annotation.name, annotation.value) + end + end + + xml.to_xml + end + + def annotation(name) + annotations.find { _1.name == name } + end + + def value_of_annotation(name) + annotation(name)&.value + end + + def annotate(name, value) + return remove_annotation(name) if value.blank? + + existing = annotation(name) + if existing + existing.value = value + else + annotations.build(name: name, value: value) + end + end + + def remove_annotation(name) + annotation(name)&.mark_for_destruction + end + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 3f4e5176e1..7b9a415a14 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -159,7 +159,9 @@ def self.all serialize :notification_settings - audited allow_mass_assignment: true + annotated + audited + state_machine initial: :incomplete do state :incomplete state :hidden @@ -370,6 +372,7 @@ def to_xml(options = {}) xml.mandatory_app_key mandatory_app_key xml.buyer_can_select_plan buyer_can_select_plan xml.buyer_plan_change_permission buyer_plan_change_permission + annotations_xml(:builder => xml) if notification_settings xml.notification_settings do |xml| diff --git a/app/representers/account_representer.rb b/app/representers/account_representer.rb index 11dc6750d0..0db5788b87 100644 --- a/app/representers/account_representer.rb +++ b/app/representers/account_representer.rb @@ -54,6 +54,7 @@ module AccountRepresenter end property :state + property :annotations_hash, as: :annotations link :self do admin_api_account_url(self) unless provider? diff --git a/app/representers/backend_api_representer.rb b/app/representers/backend_api_representer.rb index 4599e3cf8b..a10ce3cb1f 100644 --- a/app/representers/backend_api_representer.rb +++ b/app/representers/backend_api_representer.rb @@ -10,6 +10,7 @@ module BackendApiRepresenter property :system_name property :description property :private_endpoint + property :annotations_hash, as: :annotations property :account_id property :created_at property :updated_at diff --git a/app/representers/service_representer.rb b/app/representers/service_representer.rb index 2f63fe999b..75c2a40b0e 100644 --- a/app/representers/service_representer.rb +++ b/app/representers/service_representer.rb @@ -25,6 +25,7 @@ module ServiceRepresenter property :buyer_can_select_plan property :buyer_plan_change_permission property :notification_settings + property :annotations_hash, as: :annotations property :created_at property :updated_at diff --git a/db/migrate/20240726111800_create_annotation_references.rb b/db/migrate/20240726111800_create_annotation_references.rb new file mode 100644 index 0000000000..c4dfdb8efe --- /dev/null +++ b/db/migrate/20240726111800_create_annotation_references.rb @@ -0,0 +1,33 @@ +class CreateAnnotationReferences < ActiveRecord::Migration[6.1] + disable_ddl_transaction! if System::Database.postgres? + + def change + options = "CHARSET=utf8mb4 COLLATE=utf8mb4_bin" if System::Database.mysql? + + create_table :annotations, options: options do |t| + t.string :name, null: false + t.string :value + t.references :annotated, polymorphic: true, index: false, null: false + t.integer :tenant_id + t.timestamps + + t.index %i[annotated_type annotated_id name], unique: true + end + + reversible do |direction| + direction.up do + self.class.execute_trigger_action(:recreate) + end + direction.down do + self.class.execute_trigger_action(:drop) + end + end + end + + def self.execute_trigger_action(action) + trigger = System::Database.triggers.detect { |trigger| trigger.name == "annotations_tenant_id" } + + expressions = [trigger.public_send(action)].flatten + expressions.each(&ActiveRecord::Base.connection.method(:execute)) + end +end diff --git a/db/oracle_schema.rb b/db/oracle_schema.rb index 22e698f471..389c3d5e06 100644 --- a/db/oracle_schema.rb +++ b/db/oracle_schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_07_19_174715) do +ActiveRecord::Schema.define(version: 2024_07_26_111800) do create_table "access_tokens", force: :cascade do |t| t.integer "owner_id", precision: 38, null: false @@ -119,6 +119,17 @@ t.index ["timestamp"], name: "index_alerts_on_timestamp" end + create_table "annotations", force: :cascade do |t| + t.string "name", null: false + t.string "value" + t.string "annotated_type", null: false + t.integer "annotated_id", precision: 38, null: false + t.integer "tenant_id", precision: 38 + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["annotated_type", "annotated_id", "name"], name: "index_annotations_on_annotated_type_and_annotated_id_and_name", unique: true + end + create_table "api_docs_services", force: :cascade do |t| t.integer "account_id", precision: 38 t.integer "tenant_id", precision: 38 diff --git a/db/postgres_schema.rb b/db/postgres_schema.rb index 395d5aacc2..37cceef35a 100644 --- a/db/postgres_schema.rb +++ b/db/postgres_schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_07_19_174715) do +ActiveRecord::Schema.define(version: 2024_07_26_111800) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -122,6 +122,17 @@ t.index ["timestamp"], name: "index_alerts_on_timestamp" end + create_table "annotations", force: :cascade do |t| + t.string "name", null: false + t.string "value" + t.string "annotated_type", null: false + t.bigint "annotated_id", null: false + t.integer "tenant_id" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["annotated_type", "annotated_id", "name"], name: "index_annotations_on_annotated_type_and_annotated_id_and_name", unique: true + end + create_table "api_docs_services", force: :cascade do |t| t.bigint "account_id" t.bigint "tenant_id" diff --git a/db/schema.rb b/db/schema.rb index adcd8c4e5c..c2fd462e53 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_07_19_174715) do +ActiveRecord::Schema.define(version: 2024_07_26_111800) do create_table "access_tokens", charset: "utf8mb3", collation: "utf8mb3_bin", force: :cascade do |t| t.bigint "owner_id", null: false @@ -121,6 +121,17 @@ t.index ["timestamp"], name: "index_alerts_on_timestamp" end + create_table "annotations", charset: "utf8mb4", collation: "utf8mb4_bin", force: :cascade do |t| + t.string "name", null: false + t.string "value" + t.string "annotated_type", null: false + t.bigint "annotated_id", null: false + t.integer "tenant_id" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["annotated_type", "annotated_id", "name"], name: "index_annotations_on_annotated_type_and_annotated_id_and_name", unique: true + end + create_table "api_docs_services", charset: "utf8mb3", collation: "utf8mb3_bin", force: :cascade do |t| t.bigint "account_id" t.bigint "tenant_id" diff --git a/lib/system/database/definitions/mysql.rb b/lib/system/database/definitions/mysql.rb index b486f110a8..bb85d73456 100644 --- a/lib/system/database/definitions/mysql.rb +++ b/lib/system/database/definitions/mysql.rb @@ -556,6 +556,20 @@ SQL end + trigger 'annotations' do + definitions = Annotating.models.map do |model| + [ + "NEW.annotated_type = '#{model}'", + "SET NEW.tenant_id = (SELECT tenant_id FROM #{model.table_name} WHERE id = NEW.annotated_id AND tenant_id <> master_id);" + ] + end + + <<~SQL + IF #{definitions.map{ _1.join(" THEN\n") }.join("\nELSEIF ")} + END IF; + SQL + end + procedure 'sp_invoices_friendly_id', invoice_id: 'bigint' do <<~SQL BEGIN diff --git a/lib/system/database/definitions/oracle.rb b/lib/system/database/definitions/oracle.rb index f2b14493ec..f335239fca 100644 --- a/lib/system/database/definitions/oracle.rb +++ b/lib/system/database/definitions/oracle.rb @@ -572,6 +572,20 @@ SQL end + trigger 'annotations' do + definitions = Annotating.models.map do |model| + [ + ":new.annotated_type = '#{model}'", + "SELECT tenant_id INTO :new.tenant_id FROM #{model.table_name} WHERE id = :new.annotated_id AND tenant_id <> master_id;" + ] + end + + <<~SQL + IF #{definitions.map{ _1.join(" THEN\n") }.join("\nELSIF ")} + END IF; + SQL + end + procedure 'sp_invoices_friendly_id', invoice_id: 'NUMBER' do <<~SQL v_provider_account_id NUMBER; diff --git a/lib/system/database/definitions/postgres.rb b/lib/system/database/definitions/postgres.rb index 28e6ea150a..9710248eb0 100644 --- a/lib/system/database/definitions/postgres.rb +++ b/lib/system/database/definitions/postgres.rb @@ -571,6 +571,20 @@ SQL end + trigger 'annotations' do + definitions = Annotating.models.map do |model| + [ + "NEW.annotated_type = '#{model}'", + "SELECT tenant_id INTO NEW.tenant_id FROM #{model.table_name} WHERE id = NEW.annotated_id AND tenant_id <> master_id;" + ] + end + + <<~SQL + IF #{definitions.map{ _1.join(" THEN\n") }.join("\nELSEIF ")} + END IF; + SQL + end + procedure 'sp_invoices_friendly_id', invoice_id: 'numeric' do <<~SQL DECLARE diff --git a/test/factories/annotation.rb b/test/factories/annotation.rb new file mode 100644 index 0000000000..097008469f --- /dev/null +++ b/test/factories/annotation.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory(:annotation, class: Annotation) do + association :annotated, factory: :provider_account + name { Annotation::SUPPORTED_ANNOTATIONS.sample } + value { 'operator' } + end +end diff --git a/test/integration/annotating_test.rb b/test/integration/annotating_test.rb new file mode 100644 index 0000000000..71f6d42eef --- /dev/null +++ b/test/integration/annotating_test.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'test_helper' + +class AnnotatingTest < ActionDispatch::IntegrationTest + + class AnnotatedFeature < Feature + annotated + end + + class AnnotatingController < ApplicationController + def update + @annotated_feature = AnnotatedFeature.find params[:id] + annotations = params.permit(annotations: {}) + @annotated_feature.update(annotations) + end + end + + def with_test_routes + Rails.application.routes.draw do + put '/test/update' => 'annotating_test/annotating#update' + end + yield + ensure + Rails.application.routes_reloader.reload! + end + + class Update < AnnotatingTest + setup do + @annotated_feature = AnnotatedFeature.new + @annotated_feature.save! + end + + test "creates a new annotations when it doesn't exist" do + name = 'managed_by' + value = 'operator' + + with_test_routes do + put '/test/update', params: { id: @annotated_feature.id, annotations: { "#{name}": value } } + end + + annotations = @annotated_feature.reload.annotations + assert 1, annotations.size + assert_equal name, annotations.first.name + assert_equal value, annotations.first.value + end + + test 'updates an existing annotation' do + name = 'managed_by' + value = 'admin' + + @annotated_feature.annotations << Annotation.new.tap do |a| + a.name = name + a.value = 'operator' + end + @annotated_feature.save! + + with_test_routes do + put '/test/update', params: { id: @annotated_feature.id, annotations: { "#{name}": value } } + end + + annotations = @annotated_feature.reload.annotations + assert 1, annotations.size + assert_equal name, annotations.first.name + assert_equal value, annotations.first.value + end + + ['', ' ', nil].each do |value| + test "removes an annotation when it's set to #{value.inspect}" do + name = 'managed_by' + + @annotated_feature.annotations << Annotation.new.tap do |a| + a.name = name + a.value = 'operator' + end + @annotated_feature.save! + + with_test_routes do + put '/test/update', params: { id: @annotated_feature.id, annotations: [{ name: name, value: value }] } + end + + annotations = @annotated_feature.reload.annotations + assert 0, annotations.size + end + end + end +end diff --git a/test/unit/annotation_test.rb b/test/unit/annotation_test.rb new file mode 100644 index 0000000000..d404afb813 --- /dev/null +++ b/test/unit/annotation_test.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'test_helper' + +class AnnotationTest < ActiveSupport::TestCase + test 'not supported annotations are not allowed' do + subject = Annotation.new + + subject.name = 'not_supported' + + assert_not subject.valid? + assert subject.errors[:name].present? + assert subject.errors[:name].include? 'is not included in the list' + end + + test ':name is mandatory' do + subject = Annotation.new + + assert_not subject.valid? + assert subject.errors[:name].present? + assert subject.errors[:name].include? "can't be blank" + end + + test 'a valid :name is accepted' do + subject = Annotation.new + + subject.name = 'managed_by' + subject.valid? + + assert_not subject.errors[:name].present? + end + + test ':value is mandatory' do + subject = Annotation.new + + assert_not subject.valid? + assert subject.errors[:value].present? + assert subject.errors[:value].include? "can't be blank" + end + + test 'a valid :value is accepted' do + subject = Annotation.new + + subject.value = 'operator' + subject.valid? + + assert_not subject.errors[:value].present? + end + + test ':annotated is mandatory' do + subject = Annotation.new + + assert_not subject.valid? + assert subject.errors[:annotated].present? + assert subject.errors[:annotated].include? "must exist" + end + + test 'a valid :annotated is accepted' do + subject = Annotation.new + + subject.annotated = Account.new + subject.valid? + + assert_not subject.errors[:annotated].present? + end + + %i[simple_provider backend_api simple_service].each do |factory| + test "tenant_id trigger for #{factory}" do + annotated = FactoryBot.create(factory) + annotation = FactoryBot.create(:annotation, annotated: annotated) + assert annotation.reload.tenant_id + assert_equal annotated.reload.tenant_id, annotation.tenant_id + end + end +end diff --git a/test/unit/concerns/annotating/managed_by_test.rb b/test/unit/concerns/annotating/managed_by_test.rb new file mode 100644 index 0000000000..df5ab8e37f --- /dev/null +++ b/test/unit/concerns/annotating/managed_by_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Annotating + class ManagedByTest < ActiveSupport::TestCase + class AnnotatedFeature< Feature + annotated + end + + test "#managed_by returns the value of the 'managed_by' annotation" do + value = 'operator' + subject = AnnotatedFeature.new + subject.annotate('managed_by', value) + + result = subject.managed_by + + assert_equal value, result + end + + test "#managed_by returns nil when the 'managed_by' annotation doesn't exist" do + subject = AnnotatedFeature.new + + result = subject.managed_by + + assert_nil result + end + + test "#managed_by= sets the 'managed_by' annotation" do + value = 'operator' + subject = AnnotatedFeature.new + + subject.managed_by = value + + assert_equal value, subject.value_of_annotation('managed_by') + end + end +end diff --git a/test/unit/concerns/annotating/model_test.rb b/test/unit/concerns/annotating/model_test.rb new file mode 100644 index 0000000000..d839736400 --- /dev/null +++ b/test/unit/concerns/annotating/model_test.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Annotating + class ModelTest < ActiveSupport::TestCase + class AnnotatedFeature < Feature + annotated + end + + class AnnotateTest < ModelTest + test "creates a new annotation when it doesn't exist" do + name = 'managed_by' + value = 'operator' + subject = AnnotatedFeature.new + + subject.annotations.expects(:build).with(name: name, value: value) + + subject.annotate(name, value) + end + + test 'updates an annotation when it exists' do + name = 'managed_by' + value = 'admin' + subject = AnnotatedFeature.new + subject.annotate(name, 'operator') + + subject.annotate(name, value) + + assert_equal 1, subject.annotations.size + assert_equal name, subject.annotations.first.name + assert_equal value, subject.annotations.first.value + end + + ['', ' ', nil].each do |value| + test "removes an annotation when it's set to #{value.inspect}" do + name = 'managed_by' + subject = AnnotatedFeature.new + subject.annotate(name, 'operator') + + subject.expects(:remove_annotation).with(name) + + subject.annotate(name, value) + end + end + end + + class RemoveAnnotationTest < ModelTest + test 'marks annotation for destruction' do + name = 'managed_by' + subject = AnnotatedFeature.new + subject.annotate(name, 'operator') + + subject.remove_annotation(name) + + assert subject.annotations.first.marked_for_destruction? + end + end + + class AnnotationTest < ModelTest + test 'returns the annotation model' do + name = 'managed_by' + subject = AnnotatedFeature.new + subject.annotate(name, 'operator') + + result = subject.annotation(name) + + assert_instance_of Annotation, result + end + + test "returns nil if it doesn't exist" do + subject = AnnotatedFeature.new + subject.annotate('managed_by', 'operator') + + result = subject.annotation('test') + + assert_nil result + end + end + + class ValueOfAnnotationTest < ModelTest + test 'returns the annotation value' do + name = 'managed_by' + value = 'operator' + subject = AnnotatedFeature.new + subject.annotate(name, value) + + result = subject.value_of_annotation(name) + + assert_equal value, result + end + + test "returns nil if it doesn't exist" do + subject = AnnotatedFeature.new + subject.annotate('managed_by', 'operator') + + result = subject.value_of_annotation('test') + + assert_nil result + end + end + + class AnnotationsTest < ModelTest + test 'extracts the proper parameters from the given hash' do + name = :managed_by + value = 'operator' + subject = AnnotatedFeature.new + + subject.expects(:annotate).with(name, value) + + subject.annotations = { "#{name}": value } + end + + test 'annotates once per each received key' do + subject = AnnotatedFeature.new + + subject.expects(:annotate).times(3) + + subject.annotations = { managed_by: 'operator', something: 'else', foo: 'bar' } + end + end + + class AnnotationsHash < ModelTest + test 'it properly builds a hash' do + name = 'managed_by' + value = 'operator' + subject = AnnotatedFeature.new + subject.annotate(name, value) + subject.save! + + result = subject.reload.annotations_hash + + assert_equal({ name => value }, result) + end + end + + class AnnotationsXML < ModelTest + test 'it properly builds a xml' do + name = 'managed_by' + value = 'operator' + subject = AnnotatedFeature.new + subject.annotate(name, value) + + result = subject.annotations_xml + + assert_match "<#{name}>#{value}", result + end + end + end +end diff --git a/test/unit/concerns/annotating_test.rb b/test/unit/concerns/annotating_test.rb new file mode 100644 index 0000000000..1ffa3bc0e6 --- /dev/null +++ b/test/unit/concerns/annotating_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'test_helper' + +class AnnotatingTest < ActiveSupport::TestCase + + class AnnotatedFeature < Feature + annotated + end + + test 'includes the proper modules' do + %w[Annotating::Model Annotating::ManagedBy].each do |mod| + assert_includes AnnotatedFeature.ancestors.map(&:name), mod + end + end + + test '#models returns the annotated models' do + %w[Account Service BackendApi].each do |model| + assert_includes Annotating.models.map(&:name), model + end + end +end