diff --git a/.gitignore b/.gitignore index 800c71c6a..45f6fdbd2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ test_db test_db-journal .idea *.iml +test/support/database/dump.sql diff --git a/Rakefile b/Rakefile index 01619ed8e..928a92b0f 100644 --- a/Rakefile +++ b/Rakefile @@ -15,6 +15,10 @@ namespace :test do Rake::TestTask.new(:benchmark) do |t| t.pattern = 'test/benchmark/*_benchmark.rb' end + desc "Refresh dump.sql from fixtures and schema." + task :refresh_dump do + require_relative 'test/support/database/generator' + end end desc 'Test bug report template' diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index 1bb48eea2..9365b24b5 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -26,8 +26,11 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'simplecov' spec.add_development_dependency 'pry' spec.add_development_dependency 'concurrent-ruby-ext' + spec.add_development_dependency 'sequel' + spec.add_development_dependency 'sequel-rails' + spec.add_development_dependency 'activerecord', '>= 4.1' spec.add_development_dependency 'database_cleaner' - spec.add_dependency 'activerecord', '>= 4.1' + spec.add_dependency 'activesupport', '>= 4.1' spec.add_dependency 'railties', '>= 4.1' spec.add_dependency 'concurrent-ruby' end diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 0c09fb7e8..95ea71ebd 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -2,4 +2,4 @@ module JSONAPI class Resource < ActiveRelationResource root_resource end -end \ No newline at end of file +end diff --git a/lib/jsonapi/sequel_record_accessor.rb b/lib/jsonapi/sequel_record_accessor.rb new file mode 100644 index 000000000..bc60085a6 --- /dev/null +++ b/lib/jsonapi/sequel_record_accessor.rb @@ -0,0 +1,528 @@ +require 'jsonapi/record_accessor' + +module JSONAPI + class SequelRecordAccessor < RecordAccessor + + def transaction + ::Sequel.transaction(::Sequel::DATABASES) do + yield + end + end + + def rollback_transaction + fail ::Sequel::Rollback + end + + def model_error_messages(model) + model.errors + end + + def model_base_class + Sequel::Model + end + + def delete_restriction_error_class + ActiveRecord::DeleteRestrictionError + end + + def record_not_found_error_class + ActiveRecord::RecordNotFound + end + + def association_model_class_name(from_model, relationship_name) + (reflect = from_model.association_reflections[relationship_name]) && + reflect[:class_name] && reflect[:class_name].gsub(/^::/, '') # Sequel puts "::" in the beginning + end + + def find_resource(filters, options = {}) + if options[:caching] && options[:caching][:cache_serializer_output] + find_serialized_with_caching(filters, options[:caching][:serializer], options) + else + _resource_klass.resources_for(find_records(filters, options), options[:context]) + end + end + + def find_resource_by_key(key, options = {}) + if options[:caching] && options[:caching][:cache_serializer_output] + find_by_key_serialized_with_caching(key, options[:caching][:serializer], options) + else + records = find_records({ _resource_klass._primary_key => key }, options.except(:paginator, :sort_criteria)) + model = records.first + fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil? + _resource_klass.resource_for(model, options[:context]) + end + end + + def find_resources_by_keys(keys, options = {}) + records = records(options) + records = apply_includes(records, options) + records = records.where({ _resource_klass._primary_key => keys }) + + _resource_klass.resources_for(records, options[:context]) + end + + def find_count(filters, options = {}) + count_records(filter_records(filters, options)) + end + + def related_resource(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + if relationship.polymorphic? + associated_model = records_for_relationship(resource, relationship_name, options) + resource_klass = resource.class.resource_klass_for_model(associated_model) if associated_model + return resource_klass.new(associated_model, resource.context) if resource_klass && associated_model + else + resource_klass = relationship.resource_klass + if resource_klass + associated_model = records_for_relationship(resource, relationship_name, options) + return associated_model ? resource_klass.new(associated_model, resource.context) : nil + end + end + end + + def related_resources(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + relationship_resource_klass = relationship.resource_klass + + if options[:caching] && options[:caching][:cache_serializer_output] + scope = relationship_resource_klass._record_accessor.records_for_relationship(resource, relationship_name, options) + relationship_resource_klass._record_accessor.find_serialized_with_caching(scope, options[:caching][:serializer], options) + else + records = records_for_relationship(resource, relationship_name, options) + return records.collect do |record| + klass = relationship.polymorphic? ? resource.class.resource_klass_for_model(record) : relationship_resource_klass + klass.new(record, resource.context) + end + end + end + + def count_for_relationship(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + context = resource.context + + relation_name = relationship.relation_name(context: context) + records = records_for(resource, relation_name) + + resource_klass = relationship.resource_klass + + filters = options.fetch(:filters, {}) + unless filters.nil? || filters.empty? + records = resource_klass._record_accessor.apply_filters(records, filters, options) + end + + records.count(:all) + end + + def foreign_key(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + if relationship.belongs_to? + resource._model.method(relationship.foreign_key).call + else + records = records_for_relationship(resource, relationship_name, options) + return nil if records.nil? + records.public_send(relationship.resource_klass._primary_key) + end + end + + def foreign_keys(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + records = records_for_relationship(resource, relationship_name, options) + records.collect do |record| + record.public_send(relationship.resource_klass._primary_key) + end + end + + # protected-ish methods left public for tests and what not + + def find_serialized_with_caching(filters_or_source, serializer, options = {}) + if filters_or_source.is_a?(ActiveRecord::Relation) + return cached_resources_for(filters_or_source, serializer, options) + elsif _resource_klass._model_class.respond_to?(:all) && _resource_klass._model_class.respond_to?(:arel_table) + records = find_records(filters_or_source, options.except(:include_directives)) + return cached_resources_for(records, serializer, options) + else + # :nocov: + warn('Caching enabled on model that does not support ActiveRelation') + # :nocov: + end + end + + def find_by_key_serialized_with_caching(key, serializer, options = {}) + if _resource_klass._model_class.respond_to?(:all) && _resource_klass._model_class.respond_to?(:arel_table) + results = find_serialized_with_caching({ _resource_klass._primary_key => key }, serializer, options) + result = results.first + fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil? + return result + else + # :nocov: + warn('Caching enabled on model that does not support ActiveRelation') + # :nocov: + end + end + + def records_for_relationship(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + context = resource.context + + relation_name = relationship.relation_name(context: context) + records = records_for(resource, relation_name) + + resource_klass = relationship.resource_klass + + filters = options.fetch(:filters, {}) + unless filters.nil? || filters.empty? + records = resource_klass._record_accessor.apply_filters(records, filters, options) + end + + sort_criteria = options.fetch(:sort_criteria, {}) + order_options = relationship.resource_klass.construct_order_options(sort_criteria) + records = apply_sort(records, order_options, context) + + paginator = options[:paginator] + if paginator + records = apply_pagination(records, paginator, order_options) + end + + records + end + + # Implement self.records on the resource if you want to customize the relation for + # finder methods (find, find_by_key, find_serialized_with_caching) + def records(_options = {}) + if defined?(_resource_klass.records) + _resource_klass.records(_options) + else + _resource_klass._model_class.all + end + end + + # Implement records_for on the resource to customize how the associated records + # are fetched for a model. Particularly helpful for authorization. + def records_for(resource, relation_name) + if resource.respond_to?(:records_for) + return resource.records_for(relation_name) + end + + relationship = resource.class._relationships[relation_name] + + if relationship.is_a?(JSONAPI::Relationship::ToMany) + if resource.respond_to?(:"records_for_#{relation_name}") + return resource.method(:"records_for_#{relation_name}").call + end + else + if resource.respond_to?(:"record_for_#{relation_name}") + return resource.method(:"record_for_#{relation_name}").call + end + end + + resource._model.public_send(relation_name) + end + + def apply_includes(records, options = {}) + include_directives = options[:include_directives] + if include_directives + model_includes = resolve_relationship_names_to_relations(_resource_klass, include_directives.model_includes, options) + records = records.includes(model_includes) + end + + records + end + + def apply_pagination(records, paginator, order_options) + records = paginator.apply(records, order_options) if paginator + records + end + + def apply_sort(records, order_options, context = {}) + if defined?(_resource_klass.apply_sort) + _resource_klass.apply_sort(records, order_options, context) + else + if order_options.any? + order_options.each_pair do |field, direction| + if field.to_s.include?(".") + *model_names, column_name = field.split(".") + + associations = _lookup_association_chain([records.model.to_s, *model_names]) + joins_query = _build_joins([records.model, *associations]) + + # _sorting is appended to avoid name clashes with manual joins eg. overridden filters + order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}" + records = records.joins(joins_query).order(order_by_query) + else + records = records.order(field => direction) + end + end + end + + records + end + end + + def _lookup_association_chain(model_names) + associations = [] + model_names.inject do |prev, current| + association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc| + assoc.name.to_s.downcase == current.downcase + end + associations << association + association.class_name + end + + associations + end + + def _build_joins(associations) + joins = [] + + associations.inject do |prev, current| + joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}" + current + end + joins.join("\n") + end + + def apply_filter(records, filter, value, options = {}) + strategy = _resource_klass._allowed_filters.fetch(filter.to_sym, Hash.new)[:apply] + + if strategy + if strategy.is_a?(Symbol) || strategy.is_a?(String) + _resource_klass.send(strategy, records, value, options) + else + strategy.call(records, value, options) + end + else + records.where(filter => value) + end + end + + # Assumes ActiveRecord's counting. Override if you need a different counting method + def count_records(records) + records.count(:all) + end + + def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {}) + case model_includes + when Array + return model_includes.map do |value| + resolve_relationship_names_to_relations(resource_klass, value, options) + end + when Hash + model_includes.keys.each do |key| + relationship = resource_klass._relationships[key] + value = model_includes[key] + model_includes.delete(key) + model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options) + end + return model_includes + when Symbol + relationship = resource_klass._relationships[model_includes] + return relationship.relation_name(options) + end + end + + def apply_filters(records, filters, options = {}) + required_includes = [] + + if filters + filters.each do |filter, value| + if _resource_klass._relationships.include?(filter) + if _resource_klass._relationships[filter].belongs_to? + records = apply_filter(records, _resource_klass._relationships[filter].foreign_key, value, options) + else + required_includes.push(filter.to_s) + records = apply_filter(records, "#{_resource_klass._relationships[filter].table_name}.#{_resource_klass._relationships[filter].primary_key}", value, options) + end + else + records = apply_filter(records, filter, value, options) + end + end + end + + if required_includes.any? + records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(_resource_klass, required_includes, force_eager_load: true))) + end + + records + end + + def filter_records(filters, options, records = records(options)) + records = apply_filters(records, filters, options) + apply_includes(records, options) + end + + def sort_records(records, order_options, context = {}) + apply_sort(records, order_options, context) + end + + def cached_resources_for(records, serializer, options) + if _resource_klass.caching? + t = _resource_klass._model_class.arel_table + cache_ids = pluck_arel_attributes(records, t[_resource_klass._primary_key], t[_resource_klass._cache_field]) + resources = CachedResourceFragment.fetch_fragments(_resource_klass, serializer, options[:context], cache_ids) + else + resources = _resource_klass.resources_for(records, options[:context]).map { |r| [r.id, r] }.to_h + end + + preload_included_fragments(resources, records, serializer, options) + + resources.values + end + + def find_records(filters, options = {}) + if defined?(_resource_klass.find_records) + ActiveSupport::Deprecation.warn "In #{_resource_klass.name} you overrode `find_records`. "\ + "`find_records` has been deprecated in favor of using `apply` "\ + "and `verify` callables on the filter." + + _resource_klass.find_records(filters, options) + else + context = options[:context] + + records = filter_records(filters, options) + + sort_criteria = options.fetch(:sort_criteria) { [] } + order_options = _resource_klass.construct_order_options(sort_criteria) + records = sort_records(records, order_options, context) + + records = apply_pagination(records, options[:paginator], order_options) + + records + end + end + + def preload_included_fragments(resources, records, serializer, options) + return if resources.empty? + res_ids = resources.keys + + include_directives = options[:include_directives] + return unless include_directives + + context = options[:context] + + # For each association, including indirect associations, find the target record ids. + # Even if a target class doesn't have caching enabled, we still have to look up + # and match the target ids here, because we can't use ActiveRecord#includes. + # + # Note that `paths` returns partial paths before complete paths, so e.g. the partial + # fragments for posts.comments will exist before we start working with posts.comments.author + target_resources = {} + include_directives.paths.each do |path| + # If path is [:posts, :comments, :author], then... + pluck_attrs = [] # ...will be [posts.id, comments.id, authors.id, authors.updated_at] + pluck_attrs << _resource_klass._model_class.arel_table[_resource_klass._primary_key] + + relation = records + .except(:limit, :offset, :order) + .where({ _resource_klass._primary_key => res_ids }) + + # These are updated as we iterate through the association path; afterwards they will + # refer to the final resource on the path, i.e. the actual resource to find in the cache. + # So e.g. if path is [:posts, :comments, :author], then after iteration... + parent_klass = nil # Comment + klass = _resource_klass # Person + relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author + table = nil # people + assocs_path = [] # [ :posts, :approved_comments, :author ] + ar_hash = nil # { :posts => { :approved_comments => :author } } + + # For each step on the path, figure out what the actual table name/alias in the join + # will be, and include the primary key of that table in our list of fields to select + non_polymorphic = true + path.each do |elem| + relationship = klass._relationships[elem] + if relationship.polymorphic + # Can't preload through a polymorphic belongs_to association, ResourceSerializer + # will just have to bypass the cache and load the real Resource. + non_polymorphic = false + break + end + assocs_path << relationship.relation_name(options).to_sym + # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }} + ar_hash = assocs_path.reverse.reduce { |memo, step| { step => memo } } + # We can't just look up the table name from the resource class, because Arel could + # have used a table alias if the relation includes a self-reference. + join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node| + arel_node.is_a?(Arel::Nodes::InnerJoin) + end + table = join_source.left + parent_klass = klass + klass = relationship.resource_klass + pluck_attrs << table[klass._primary_key] + end + next unless non_polymorphic + + # Pre-fill empty hashes for each resource up to the end of the path. + # This allows us to later distinguish between a preload that returned nothing + # vs. a preload that never ran. + prefilling_resources = resources.values + path.each do |rel_name| + rel_name = serializer.key_formatter.format(rel_name) + prefilling_resources.map! do |res| + res.preloaded_fragments[rel_name] ||= {} + res.preloaded_fragments[rel_name].values + end + prefilling_resources.flatten!(1) + end + + pluck_attrs << table[klass._cache_field] if klass.caching? + relation = relation.joins(ar_hash) + if relationship.is_a?(JSONAPI::Relationship::ToMany) + # Rails doesn't include order clauses in `joins`, so we have to add that manually here. + # FIXME Should find a better way to reflect on relationship ordering. :-( + relation = relation.order(parent_klass._model_class.new.send(assocs_path.last).arel.orders) + end + + # [[post id, comment id, author id, author updated_at], ...] + id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs) + + target_resources[klass.name] ||= {} + + if klass.caching? + sub_cache_ids = id_rows + .map { |row| row.last(2) } + .reject { |row| target_resources[klass.name].has_key?(row.first) } + .uniq + target_resources[klass.name].merge! CachedResourceFragment.fetch_fragments( + klass, serializer, context, sub_cache_ids + ) + else + sub_res_ids = id_rows + .map(&:last) + .reject { |id| target_resources[klass.name].has_key?(id) } + .uniq + found = klass.find({ klass._primary_key => sub_res_ids }, context: options[:context]) + target_resources[klass.name].merge! found.map { |r| [r.id, r] }.to_h + end + + id_rows.each do |row| + res = resources[row.first] + path.each_with_index do |rel_name, index| + rel_name = serializer.key_formatter.format(rel_name) + rel_id = row[index+1] + assoc_rels = res.preloaded_fragments[rel_name] + if index == path.length - 1 + assoc_rels[rel_id] = target_resources[klass.name].fetch(rel_id) + else + res = assoc_rels[rel_id] + end + end + end + end + end + + def pluck_arel_attributes(relation, *attrs) + conn = relation.connection + quoted_attrs = attrs.map do |attr| + quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name) + quoted_column = conn.quote_column_name(attr.name) + "#{quoted_table}.#{quoted_column}" + end + relation.pluck(*quoted_attrs) + end + end +end \ No newline at end of file diff --git a/test/support/active_record/app_config.rb b/test/support/active_record/app_config.rb new file mode 100644 index 000000000..f33f59f5b --- /dev/null +++ b/test/support/active_record/app_config.rb @@ -0,0 +1,13 @@ +TestApp.class_eval do + config.active_record.schema_format = :none + + if Rails::VERSION::MAJOR >= 5 + config.active_support.halt_callback_chains_on_return_false = false + config.active_record.time_zone_aware_types = [:time, :datetime] + config.active_record.belongs_to_required_by_default = false + + if Rails::VERSION::MINOR >= 2 + config.active_record.sqlite3.represent_boolean_as_integer = true + end + end +end diff --git a/test/support/active_record/import_schema.rb b/test/support/active_record/import_schema.rb new file mode 100644 index 000000000..07c4ce254 --- /dev/null +++ b/test/support/active_record/import_schema.rb @@ -0,0 +1,11 @@ +require 'active_record' + +connection = ActiveRecord::Base.connection + +sql = File.read(File.expand_path('../../database/dump.sql', __FILE__)) +statements = sql.split(/;$/) +statements.pop # the last empty statement + +statements.each do |statement| + connection.execute(statement) +end diff --git a/test/support/active_record/initialize.rb b/test/support/active_record/initialize.rb new file mode 100644 index 000000000..1f7ec46c0 --- /dev/null +++ b/test/support/active_record/initialize.rb @@ -0,0 +1 @@ +require 'active_record/railtie' \ No newline at end of file diff --git a/test/support/active_record/models.rb b/test/support/active_record/models.rb new file mode 100644 index 000000000..6d7c1bf1e --- /dev/null +++ b/test/support/active_record/models.rb @@ -0,0 +1,460 @@ +class Session < ActiveRecord::Base + self.primary_key = "id" + has_many :responses +end + +class Response < ActiveRecord::Base + belongs_to :session + has_one :paragraph, :class_name => "ResponseText::Paragraph" + + def response_type + case self.type + when "Response::SingleTextbox" + "single_textbox" + else + "question" + end + end + def response_type=type + self.type = case type + when "single_textbox" + "Response::SingleTextbox" + else + "Response" + end + end +end + +class Response::SingleTextbox < Response + has_one :paragraph, :class_name => "ResponseText::Paragraph", :foreign_key => :response_id +end + +class ResponseText < ActiveRecord::Base +end + +class ResponseText::Paragraph < ResponseText +end + +class Person < ActiveRecord::Base + has_many :posts, foreign_key: 'author_id' + has_many :comments, foreign_key: 'author_id' + has_many :book_comments, foreign_key: 'author_id' + has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception + has_many :vehicles + belongs_to :preferences + belongs_to :hair_cut + has_one :author_detail + + has_and_belongs_to_many :books, join_table: :book_authors + has_and_belongs_to_many :not_banned_books, -> { merge(Book.not_banned) }, + class_name: 'Book', + join_table: :book_authors + + has_many :even_posts, -> { where('posts.id % 2 = 0') }, class_name: 'Post', foreign_key: 'author_id' + has_many :odd_posts, -> { where('posts.id % 2 = 1') }, class_name: 'Post', foreign_key: 'author_id' + + has_many :pictures, foreign_key: 'author_id' + + ### Validations + validates :name, presence: true + validates :date_joined, presence: true +end + +class AuthorDetail < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'person_id' +end + +class Post < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :writer, class_name: 'Person', foreign_key: 'author_id' + has_many :comments + has_and_belongs_to_many :tags, join_table: :posts_tags + has_many :special_post_tags, source: :tag + has_many :special_tags, through: :special_post_tags, source: :tag + belongs_to :section + belongs_to :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' + + validates :author, presence: true + validates :title, length: { maximum: 35 } + + before_destroy :destroy_callback + + def destroy_callback + case title + when "can't destroy me", "can't destroy me either" + errors.add(:base, "can't destroy me") + + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + when "locked title" + errors.add(:title, "is locked") + + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class SpecialPostTag < ActiveRecord::Base + belongs_to :tag + belongs_to :post +end + +class Comment < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :post + has_and_belongs_to_many :tags, join_table: :comments_tags +end + +class Company < ActiveRecord::Base +end + +class Firm < Company +end + +class Tag < ActiveRecord::Base + has_and_belongs_to_many :posts, join_table: :posts_tags + has_and_belongs_to_many :planets, join_table: :planets_tags + + has_and_belongs_to_many :comments, join_table: :comments_tags +end + +class Section < ActiveRecord::Base + has_many :posts +end + +class HairCut < ActiveRecord::Base + has_many :people +end + +class Property < ActiveRecord::Base +end + +class Customer < ActiveRecord::Base +end + +class BadlyNamedAttributes < ActiveRecord::Base +end + +class Cat < ActiveRecord::Base +end + +class IsoCurrency < ActiveRecord::Base + self.primary_key = :code + has_many :expense_entries, foreign_key: 'currency_code' +end + +class ExpenseEntry < ActiveRecord::Base + belongs_to :employee, class_name: 'Person', foreign_key: 'employee_id' + belongs_to :iso_currency, foreign_key: 'currency_code' +end + +class Planet < ActiveRecord::Base + has_many :moons + belongs_to :planet_type + + has_and_belongs_to_many :tags, join_table: :planets_tags + + # Test model callback cancelling save + before_save :check_not_pluto + + def check_not_pluto + # Pluto can't be a planet, so cancel the save + if name.downcase == 'pluto' + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class PlanetType < ActiveRecord::Base + has_many :planets +end + +class Moon < ActiveRecord::Base + belongs_to :planet + + has_many :craters +end + +class Crater < ActiveRecord::Base + self.primary_key = :code + + belongs_to :moon +end + +class Preferences < ActiveRecord::Base + has_one :author, class_name: 'Person', :inverse_of => 'preferences' +end + +class Fact < ActiveRecord::Base + validates :spouse_name, :bio, presence: true +end + +class Like < ActiveRecord::Base +end + +class Breed + include ActiveModel::Model + + def initialize(id = nil, name = nil) + if id.nil? + @id = $breed_data.new_id + $breed_data.add(self) + else + @id = id + end + @name = name + @errors = ActiveModel::Errors.new(self) + end + + attr_accessor :id, :name + + def destroy + $breed_data.remove(@id) + end + + validates :name, presence: true +end + +class Book < ActiveRecord::Base + has_many :book_comments + has_many :approved_book_comments, -> { where(approved: true) }, class_name: "BookComment" + + has_and_belongs_to_many :authors, join_table: :book_authors, class_name: "Person" + + scope :not_banned, -> { + where(banned: false) + } +end + +class BookComment < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :book + + def self.for_user(current_user) + records = self.all + # Hide the unapproved comments from people who are not book admins + unless current_user && current_user.book_admin + records = records.where(approved: true) + end + records + end +end + +class BreedData + def initialize + @breeds = {} + end + + def breeds + @breeds + end + + def new_id + @breeds.keys.max + 1 + end + + def add(breed) + @breeds[breed.id] = breed + end + + def remove(id) + @breeds.delete(id) + end +end + +class Customer < ActiveRecord::Base + has_many :purchase_orders +end + +class PurchaseOrder < ActiveRecord::Base + belongs_to :customer + has_many :line_items + has_many :admin_line_items, class_name: 'LineItem', foreign_key: 'purchase_order_id' + + has_and_belongs_to_many :order_flags, join_table: :purchase_orders_order_flags + + has_and_belongs_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class_name: 'OrderFlag' +end + +class OrderFlag < ActiveRecord::Base + has_and_belongs_to_many :purchase_orders, join_table: :purchase_orders_order_flags +end + +class LineItem < ActiveRecord::Base + belongs_to :purchase_order +end + +class NumeroTelefone < ActiveRecord::Base +end + +class Category < ActiveRecord::Base +end + +class Picture < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + + belongs_to :imageable, polymorphic: true + belongs_to :document, -> { where( pictures: { imageable_type: 'Document' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' + belongs_to :product, -> { where( pictures: { imageable_type: 'Product' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' + + has_one :file_properties, as: 'fileable' +end + +class Vehicle < ActiveRecord::Base + belongs_to :person +end + +class Car < Vehicle +end + +class Boat < Vehicle +end + +class Document < ActiveRecord::Base + has_many :pictures, as: :imageable + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + has_one :file_properties, as: 'fileable' +end + +class Product < ActiveRecord::Base + has_many :pictures, as: :imageable + belongs_to :designer, class_name: 'Person', foreign_key: 'designer_id' + has_one :file_properties, as: 'fileable' +end + +class FileProperties < ActiveRecord::Base + belongs_to :fileable, polymorphic: true + belongs_to :tag +end + +class Make < ActiveRecord::Base +end + +class WebPage < ActiveRecord::Base +end + +class Box < ActiveRecord::Base + has_many :things +end + +class User < ActiveRecord::Base + has_many :things +end + +class Thing < ActiveRecord::Base + belongs_to :box + belongs_to :user + + has_many :related_things, foreign_key: :from_id + has_many :things, through: :related_things, source: :to +end + +class RelatedThing < ActiveRecord::Base + belongs_to :from, class_name: "Thing", foreign_key: :from_id + belongs_to :to, class_name: "Thing", foreign_key: :to_id +end + +class Question < ActiveRecord::Base + has_one :answer + + def respondent + answer.try(:respondent) + end +end + +class Answer < ActiveRecord::Base + belongs_to :question + belongs_to :respondent, polymorphic: true +end + +class Patient < ActiveRecord::Base +end + +class Doctor < ActiveRecord::Base +end + +module Api + module V7 + class Client < Customer + end + + class Customer < Customer + end + end +end + +class Storage < ActiveRecord::Base + has_one :keeper, class_name: 'Keeper', as: :keepable +end + +class Keeper < ActiveRecord::Base + belongs_to :keepable, polymorphic: true +end + +class AccessCard < ActiveRecord::Base + has_many :workers +end + +class Worker < ActiveRecord::Base + belongs_to :access_card +end + +class Agency < ActiveRecord::Base +end + +class Indicator < ActiveRecord::Base + belongs_to :agency + has_many :widgets, primary_key: :import_id, foreign_key: :indicator_import_id +end + +class Widget < ActiveRecord::Base + belongs_to :indicator, primary_key: :import_id, foreign_key: :indicator_import_id +end + +class Robot < ActiveRecord::Base +end + +class Painter < ActiveRecord::Base + has_many :paintings +end + +class Painting < ActiveRecord::Base + belongs_to :painter + has_many :collectors +end + +class Collector < ActiveRecord::Base + belongs_to :painting +end + +class List < ActiveRecord::Base + has_many :items, class_name: 'ListItem', inverse_of: :list +end + +class ListItem < ActiveRecord::Base + belongs_to :list, inverse_of: :items +end + +### PORO Data - don't do this in a production app +$breed_data = BreedData.new +$breed_data.add(Breed.new(0, 'persian')) +$breed_data.add(Breed.new(1, 'siamese')) +$breed_data.add(Breed.new(2, 'sphinx')) +$breed_data.add(Breed.new(3, 'to_delete')) diff --git a/test/support/active_record/rollback.rb b/test/support/active_record/rollback.rb new file mode 100644 index 000000000..cbd06d4a8 --- /dev/null +++ b/test/support/active_record/rollback.rb @@ -0,0 +1,21 @@ +module Minitest + module Rollback + + def before_setup + ActiveRecord::Base.connection.begin_transaction joinable: false + super + end + + def after_teardown + super + conn = ActiveRecord::Base.connection + conn.rollback_transaction if conn.transaction_open? + ActiveRecord::Base.clear_active_connections! + end + + end + + class Test + include Rollback + end +end diff --git a/test/support/active_record/setup.rb b/test/support/active_record/setup.rb new file mode 100644 index 000000000..f0c94dfcb --- /dev/null +++ b/test/support/active_record/setup.rb @@ -0,0 +1,29 @@ +TestApp.class_eval do + config.active_record.schema_format = :none + + if Rails::VERSION::MAJOR >= 5 + config.active_support.halt_callback_chains_on_return_false = false + config.active_record.time_zone_aware_types = [:time, :datetime] + config.active_record.belongs_to_required_by_default = false + if Rails::VERSION::MINOR >= 2 + config.active_record.sqlite3.represent_boolean_as_integer = true + end + end +end + +class Minitest::Test + include ActiveRecord::TestFixtures + + self.fixture_path = "#{Rails.root}/fixtures" + fixtures :all +end + +class ActiveSupport::TestCase + self.fixture_path = "#{Rails.root}/fixtures" + fixtures :all +end + +class ActionDispatch::IntegrationTest + self.fixture_path = "#{Rails.root}/fixtures" + fixtures :all +end diff --git a/test/fixtures/active_record.rb b/test/support/controllers_resources_processors.rb similarity index 66% rename from test/fixtures/active_record.rb rename to test/support/controllers_resources_processors.rb index 68bb69fc3..2af166215 100644 --- a/test/fixtures/active_record.rb +++ b/test/support/controllers_resources_processors.rb @@ -1,890 +1,4 @@ -require 'active_record' -require 'jsonapi-resources' - -ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.uncountable 'preferences' - inflect.uncountable 'file_properties' - inflect.irregular 'numero_telefone', 'numeros_telefone' -end - -### DATABASE -ActiveRecord::Schema.define do - create_table :sessions, id: false, force: true do |t| - t.string :id, :limit => 36, :primary_key => true, null: false - t.string :survey_id, :limit => 36, null: false - - t.timestamps - end - - create_table :responses, force: true do |t| - #t.string :id, :limit => 36, :primary_key => true, null: false - - t.string :session_id, limit: 36, null: false - - t.string :type - t.string :question_id, limit: 36 - - t.timestamps - end - - create_table :response_texts, force: true do |t| - t.text :text - t.integer :response_id - - t.timestamps - end - - create_table :people, force: true do |t| - t.string :name - t.string :email - t.datetime :date_joined - t.belongs_to :preferences - t.integer :hair_cut_id, index: true - t.boolean :book_admin, default: false - t.boolean :special, default: false - t.timestamps null: false - end - - create_table :author_details, force: true do |t| - t.integer :person_id - t.string :author_stuff - t.timestamps null: false - end - - create_table :posts, force: true do |t| - t.string :title, length: 255 - t.text :body - t.integer :author_id - t.integer :parent_post_id - t.belongs_to :section, index: true - t.timestamps null: false - end - - create_table :comments, force: true do |t| - t.text :body - t.belongs_to :post, index: true - t.integer :author_id - t.timestamps null: false - end - - create_table :companies, force: true do |t| - t.string :type - t.string :name - t.string :address - t.timestamps null: false - end - - create_table :tags, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :sections, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :posts_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :posts_tags, [:post_id, :tag_id], unique: true - - create_table :special_post_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :special_post_tags, [:post_id, :tag_id], unique: true - - create_table :comments_tags, force: true do |t| - t.references :comment, :tag, index: true - end - - create_table :iso_currencies, id: false, force: true do |t| - t.string :code, limit: 3, null: false - t.string :name - t.string :country_name - t.string :minor_unit - t.timestamps null: false - end - add_index :iso_currencies, :code, unique: true - - create_table :expense_entries, force: true do |t| - t.string :currency_code, limit: 3, null: false - t.integer :employee_id, null: false - t.decimal :cost, precision: 12, scale: 4, null: false - t.date :transaction_date - t.timestamps null: false - end - - create_table :planets, force: true do |t| - t.string :name - t.string :description - t.integer :planet_type_id - end - - create_table :planets_tags, force: true do |t| - t.references :planet, :tag, index: true - end - add_index :planets_tags, [:planet_id, :tag_id], unique: true - - create_table :planet_types, force: true do |t| - t.string :name - end - - create_table :moons, force: true do |t| - t.string :name - t.string :description - t.integer :planet_id - t.timestamps null: false - end - - create_table :craters, id: false, force: true do |t| - t.string :code - t.string :description - t.integer :moon_id - t.timestamps null: false - end - - create_table :preferences, force: true do |t| - t.integer :person_id - t.boolean :advanced_mode, default: false - t.string :nickname - t.timestamps null: false - end - - create_table :facts, force: true do |t| - t.integer :person_id - t.string :spouse_name - t.text :bio - t.float :quality_rating - t.decimal :salary, precision: 12, scale: 2 - t.datetime :date_time_joined - t.date :birthday - t.time :bedtime - t.binary :photo, limit: 1.kilobyte - t.boolean :cool - t.timestamps null: false - end - - create_table :books, force: true do |t| - t.string :title - t.string :isbn - t.boolean :banned, default: false - t.timestamps null: false - end - - create_table :book_authors, force: true do |t| - t.integer :book_id - t.integer :person_id - end - - create_table :book_comments, force: true do |t| - t.text :body - t.belongs_to :book, index: true - t.integer :author_id - t.boolean :approved, default: true - t.timestamps null: false - end - - create_table :customers, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :purchase_orders, force: true do |t| - t.date :order_date - t.date :requested_delivery_date - t.date :delivery_date - t.integer :customer_id - t.string :delivery_name - t.string :delivery_address_1 - t.string :delivery_address_2 - t.string :delivery_city - t.string :delivery_state - t.string :delivery_postal_code - t.float :delivery_fee - t.float :tax - t.float :total - t.timestamps null: false - end - - create_table :order_flags, force: true do |t| - t.string :name - end - - create_table :purchase_orders_order_flags, force: true do |t| - t.references :purchase_order, :order_flag, index: true - end - add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" - - create_table :line_items, force: true do |t| - t.integer :purchase_order_id - t.string :part_number - t.string :quantity - t.float :item_cost - t.timestamps null: false - end - - create_table :hair_cuts, force: true do |t| - t.string :style - end - - create_table :numeros_telefone, force: true do |t| - t.string :numero_telefone - t.timestamps null: false - end - - create_table :categories, force: true do |t| - t.string :name - t.string :status, limit: 10 - t.timestamps null: false - end - - create_table :pictures, force: true do |t| - t.string :name - t.integer :author_id - t.references :imageable, polymorphic: true, index: true - t.timestamps null: false - end - - create_table :documents, force: true do |t| - t.string :name - t.integer :author_id - t.timestamps null: false - end - - create_table :products, force: true do |t| - t.string :name - t.integer :designer_id - t.timestamps null: false - end - - create_table :file_properties, force: true do |t| - t.string :name - t.timestamps null: false - t.references :fileable, polymorphic: true, index: true - t.belongs_to :tag, index: true - - t.integer :size - end - - create_table :vehicles, force: true do |t| - t.string :type - t.string :make - t.string :model - t.string :length_at_water_line - t.string :drive_layout - t.string :serial_number - t.integer :person_id - t.timestamps null: false - end - - create_table :makes, force: true do |t| - t.string :model - t.timestamps null: false - end - - # special cases - fields that look like they should be reserved names - create_table :hrefs, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :links, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :web_pages, force: true do |t| - t.string :href - t.string :link - t.timestamps null: false - end - - create_table :questionables, force: true do |t| - t.timestamps null: false - end - - create_table :boxes, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :things, force: true do |t| - t.string :name - t.references :user - t.references :box - - t.timestamps null: false - end - - create_table :users, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :related_things, force: true do |t| - t.string :name - t.references :from, references: :thing - t.references :to, references: :thing - - t.timestamps null: false - end - - create_table :questions, force: true do |t| - t.string :text - t.timestamps null: false - end - - create_table :answers, force: true do |t| - t.references :question - t.integer :respondent_id - t.string :respondent_type - t.string :text - t.timestamps null: false - end - - create_table :patients, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :doctors, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :painters, force: true do |t| - t.string :name - - t.timestamps null: false - end - - create_table :paintings, force: true do |t| - t.string :title - t.string :category - t.belongs_to :painter - - t.timestamps null: false - end - - create_table :collectors, force: true do |t| - t.string :name - t.belongs_to :painting - end - - create_table :lists, force: true do |t| - t.string :name - end - - create_table :list_items, force: true do |t| - t.belongs_to :list - end - - # special cases - create_table :storages, force: true do |t| - t.string :token, null: false - t.string :name - t.timestamps null: false - end - - create_table :keepers, force: true do |t| - t.string :name - t.string :keepable_type, null: false - t.integer :keepable_id, null: false - t.timestamps null: false - end - - create_table :access_cards, force: true do |t| - t.string :token, null: false - t.string :security_level - t.timestamps null: false - end - - create_table :workers, force: true do |t| - t.string :name - t.integer :access_card_id, null: false - t.timestamps null: false - end - - create_table :agencies, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :indicators, force: true do |t| - t.string :name - t.string :import_id - t.integer :agency_id, null: false - t.timestamps null: false - end - - create_table :widgets, force: true do |t| - t.string :name - t.string :indicator_import_id, null: false - t.timestamps null: false - end - - create_table :robots, force: true do |t| - t.string :name - t.integer :version - t.timestamps null: false - end -end - -### MODELS -class Session < ActiveRecord::Base - self.primary_key = "id" - has_many :responses -end - -class Response < ActiveRecord::Base - belongs_to :session - has_one :paragraph, :class_name => "ResponseText::Paragraph" - - def response_type - case self.type - when "Response::SingleTextbox" - "single_textbox" - else - "question" - end - end - def response_type=type - self.type = case type - when "single_textbox" - "Response::SingleTextbox" - else - "Response" - end - end -end - -class Response::SingleTextbox < Response - has_one :paragraph, :class_name => "ResponseText::Paragraph", :foreign_key => :response_id -end - -class ResponseText < ActiveRecord::Base -end - -class ResponseText::Paragraph < ResponseText -end - -class Person < ActiveRecord::Base - has_many :posts, foreign_key: 'author_id' - has_many :comments, foreign_key: 'author_id' - has_many :book_comments, foreign_key: 'author_id' - has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception - has_many :vehicles - belongs_to :preferences - belongs_to :hair_cut - has_one :author_detail - - has_and_belongs_to_many :books, join_table: :book_authors - has_and_belongs_to_many :not_banned_books, -> { merge(Book.not_banned) }, - class_name: 'Book', - join_table: :book_authors - - has_many :even_posts, -> { where('posts.id % 2 = 0') }, class_name: 'Post', foreign_key: 'author_id' - has_many :odd_posts, -> { where('posts.id % 2 = 1') }, class_name: 'Post', foreign_key: 'author_id' - - has_many :pictures, foreign_key: 'author_id' - - ### Validations - validates :name, presence: true - validates :date_joined, presence: true -end - -class AuthorDetail < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'person_id' -end - -class Post < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :writer, class_name: 'Person', foreign_key: 'author_id' - has_many :comments - has_and_belongs_to_many :tags, join_table: :posts_tags - has_many :special_post_tags, source: :tag - has_many :special_tags, through: :special_post_tags, source: :tag - belongs_to :section - belongs_to :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' - - validates :author, presence: true - validates :title, length: { maximum: 35 } - - before_destroy :destroy_callback - - def destroy_callback - case title - when "can't destroy me", "can't destroy me either" - errors.add(:base, "can't destroy me") - - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - when "locked title" - errors.add(:title, "is locked") - - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - end - end -end - -class SpecialPostTag < ActiveRecord::Base - belongs_to :tag - belongs_to :post -end - -class Comment < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :post - has_and_belongs_to_many :tags, join_table: :comments_tags -end - -class Company < ActiveRecord::Base -end - -class Firm < Company -end - -class Tag < ActiveRecord::Base - has_and_belongs_to_many :posts, join_table: :posts_tags - has_and_belongs_to_many :planets, join_table: :planets_tags - - has_and_belongs_to_many :comments, join_table: :comments_tags -end - -class Section < ActiveRecord::Base - has_many :posts -end - -class HairCut < ActiveRecord::Base - has_many :people -end - -class Property < ActiveRecord::Base -end - -class Customer < ActiveRecord::Base -end - -class BadlyNamedAttributes < ActiveRecord::Base -end - -class Cat < ActiveRecord::Base -end - -class IsoCurrency < ActiveRecord::Base - self.primary_key = :code - has_many :expense_entries, foreign_key: 'currency_code' -end - -class ExpenseEntry < ActiveRecord::Base - belongs_to :employee, class_name: 'Person', foreign_key: 'employee_id' - belongs_to :iso_currency, foreign_key: 'currency_code' -end - -class Planet < ActiveRecord::Base - has_many :moons - belongs_to :planet_type - - has_and_belongs_to_many :tags, join_table: :planets_tags - - # Test model callback cancelling save - before_save :check_not_pluto - - def check_not_pluto - # Pluto can't be a planet, so cancel the save - if name.downcase == 'pluto' - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - end - end -end - -class PlanetType < ActiveRecord::Base - has_many :planets -end - -class Moon < ActiveRecord::Base - belongs_to :planet - - has_many :craters -end - -class Crater < ActiveRecord::Base - self.primary_key = :code - - belongs_to :moon -end - -class Preferences < ActiveRecord::Base - has_one :author, class_name: 'Person', :inverse_of => 'preferences' -end - -class Fact < ActiveRecord::Base - validates :spouse_name, :bio, presence: true -end - -class Like < ActiveRecord::Base -end - -class Breed - include ActiveModel::Model - - def initialize(id = nil, name = nil) - if id.nil? - @id = $breed_data.new_id - $breed_data.add(self) - else - @id = id - end - @name = name - @errors = ActiveModel::Errors.new(self) - end - - attr_accessor :id, :name - - def destroy - $breed_data.remove(@id) - end - - validates :name, presence: true -end - -class Book < ActiveRecord::Base - has_many :book_comments - has_many :approved_book_comments, -> { where(approved: true) }, class_name: "BookComment" - - has_and_belongs_to_many :authors, join_table: :book_authors, class_name: "Person" - - scope :not_banned, -> { - where(banned: false) - } -end - -class BookComment < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :book - - def self.for_user(current_user) - records = self.all - # Hide the unapproved comments from people who are not book admins - unless current_user && current_user.book_admin - records = records.where(approved: true) - end - records - end -end - -class BreedData - def initialize - @breeds = {} - end - - def breeds - @breeds - end - - def new_id - @breeds.keys.max + 1 - end - - def add(breed) - @breeds[breed.id] = breed - end - - def remove(id) - @breeds.delete(id) - end -end - -class Customer < ActiveRecord::Base - has_many :purchase_orders -end - -class PurchaseOrder < ActiveRecord::Base - belongs_to :customer - has_many :line_items - has_many :admin_line_items, class_name: 'LineItem', foreign_key: 'purchase_order_id' - - has_and_belongs_to_many :order_flags, join_table: :purchase_orders_order_flags - - has_and_belongs_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class_name: 'OrderFlag' -end - -class OrderFlag < ActiveRecord::Base - has_and_belongs_to_many :purchase_orders, join_table: :purchase_orders_order_flags -end - -class LineItem < ActiveRecord::Base - belongs_to :purchase_order -end - -class NumeroTelefone < ActiveRecord::Base -end - -class Category < ActiveRecord::Base -end - -class Picture < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - - belongs_to :imageable, polymorphic: true - belongs_to :document, -> { where( pictures: { imageable_type: 'Document' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' - belongs_to :product, -> { where( pictures: { imageable_type: 'Product' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' - - has_one :file_properties, as: 'fileable' -end - -class Vehicle < ActiveRecord::Base - belongs_to :person -end - -class Car < Vehicle -end - -class Boat < Vehicle -end - -class Document < ActiveRecord::Base - has_many :pictures, as: :imageable - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - has_one :file_properties, as: 'fileable' -end - -class Product < ActiveRecord::Base - has_many :pictures, as: :imageable - belongs_to :designer, class_name: 'Person', foreign_key: 'designer_id' - has_one :file_properties, as: 'fileable' -end - -class FileProperties < ActiveRecord::Base - belongs_to :fileable, polymorphic: true - belongs_to :tag -end - -class Make < ActiveRecord::Base -end - -class WebPage < ActiveRecord::Base -end - -class Box < ActiveRecord::Base - has_many :things -end - -class User < ActiveRecord::Base - has_many :things -end - -class Thing < ActiveRecord::Base - belongs_to :box - belongs_to :user - - has_many :related_things, foreign_key: :from_id - has_many :things, through: :related_things, source: :to -end - -class RelatedThing < ActiveRecord::Base - belongs_to :from, class_name: "Thing", foreign_key: :from_id - belongs_to :to, class_name: "Thing", foreign_key: :to_id -end - -class Question < ActiveRecord::Base - has_one :answer - - def respondent - answer.try(:respondent) - end -end - -class Answer < ActiveRecord::Base - belongs_to :question - belongs_to :respondent, polymorphic: true -end - -class Patient < ActiveRecord::Base -end - -class Doctor < ActiveRecord::Base -end - -module Api - module V7 - class Client < Customer - end - - class Customer < Customer - end - end -end - -class Storage < ActiveRecord::Base - has_one :keeper, class_name: 'Keeper', as: :keepable -end - -class Keeper < ActiveRecord::Base - belongs_to :keepable, polymorphic: true -end - -class AccessCard < ActiveRecord::Base - has_many :workers -end - -class Worker < ActiveRecord::Base - belongs_to :access_card -end - -class Agency < ActiveRecord::Base -end - -class Indicator < ActiveRecord::Base - belongs_to :agency - has_many :widgets, primary_key: :import_id, foreign_key: :indicator_import_id -end - -class Widget < ActiveRecord::Base - belongs_to :indicator, primary_key: :import_id, foreign_key: :indicator_import_id -end - -class Robot < ActiveRecord::Base -end - -class Painter < ActiveRecord::Base - has_many :paintings -end - -class Painting < ActiveRecord::Base - belongs_to :painter - has_many :collectors -end - -class Collector < ActiveRecord::Base - belongs_to :painting -end - -class List < ActiveRecord::Base - has_many :items, class_name: 'ListItem', inverse_of: :list -end - -class ListItem < ActiveRecord::Base - belongs_to :list, inverse_of: :items -end +# Controllers, Resources, and Processors for specs. ### CONTROLLERS class SessionsController < ActionController::Base diff --git a/test/support/database/config.yml b/test/support/database/config.yml new file mode 100644 index 000000000..0cda30abf --- /dev/null +++ b/test/support/database/config.yml @@ -0,0 +1,5 @@ +test: + adapter: sqlite3 + database: test_db + pool: 5 + timeout: 5000 diff --git a/test/fixtures/answers.yml b/test/support/database/fixtures/answers.yml similarity index 100% rename from test/fixtures/answers.yml rename to test/support/database/fixtures/answers.yml diff --git a/test/fixtures/author_details.yml b/test/support/database/fixtures/author_details.yml similarity index 100% rename from test/fixtures/author_details.yml rename to test/support/database/fixtures/author_details.yml diff --git a/test/fixtures/book_authors.yml b/test/support/database/fixtures/book_authors.yml similarity index 100% rename from test/fixtures/book_authors.yml rename to test/support/database/fixtures/book_authors.yml diff --git a/test/fixtures/book_comments.yml b/test/support/database/fixtures/book_comments.yml similarity index 100% rename from test/fixtures/book_comments.yml rename to test/support/database/fixtures/book_comments.yml diff --git a/test/fixtures/books.yml b/test/support/database/fixtures/books.yml similarity index 100% rename from test/fixtures/books.yml rename to test/support/database/fixtures/books.yml diff --git a/test/fixtures/boxes.yml b/test/support/database/fixtures/boxes.yml similarity index 100% rename from test/fixtures/boxes.yml rename to test/support/database/fixtures/boxes.yml diff --git a/test/fixtures/categories.yml b/test/support/database/fixtures/categories.yml similarity index 100% rename from test/fixtures/categories.yml rename to test/support/database/fixtures/categories.yml diff --git a/test/fixtures/comments.yml b/test/support/database/fixtures/comments.yml similarity index 100% rename from test/fixtures/comments.yml rename to test/support/database/fixtures/comments.yml diff --git a/test/fixtures/comments_tags.yml b/test/support/database/fixtures/comments_tags.yml similarity index 100% rename from test/fixtures/comments_tags.yml rename to test/support/database/fixtures/comments_tags.yml diff --git a/test/fixtures/companies.yml b/test/support/database/fixtures/companies.yml similarity index 100% rename from test/fixtures/companies.yml rename to test/support/database/fixtures/companies.yml diff --git a/test/fixtures/craters.yml b/test/support/database/fixtures/craters.yml similarity index 100% rename from test/fixtures/craters.yml rename to test/support/database/fixtures/craters.yml diff --git a/test/fixtures/customers.yml b/test/support/database/fixtures/customers.yml similarity index 100% rename from test/fixtures/customers.yml rename to test/support/database/fixtures/customers.yml diff --git a/test/fixtures/doctors.yml b/test/support/database/fixtures/doctors.yml similarity index 100% rename from test/fixtures/doctors.yml rename to test/support/database/fixtures/doctors.yml diff --git a/test/fixtures/documents.yml b/test/support/database/fixtures/documents.yml similarity index 100% rename from test/fixtures/documents.yml rename to test/support/database/fixtures/documents.yml diff --git a/test/fixtures/expense_entries.yml b/test/support/database/fixtures/expense_entries.yml similarity index 100% rename from test/fixtures/expense_entries.yml rename to test/support/database/fixtures/expense_entries.yml diff --git a/test/fixtures/facts.yml b/test/support/database/fixtures/facts.yml similarity index 100% rename from test/fixtures/facts.yml rename to test/support/database/fixtures/facts.yml diff --git a/test/fixtures/hair_cuts.yml b/test/support/database/fixtures/hair_cuts.yml similarity index 100% rename from test/fixtures/hair_cuts.yml rename to test/support/database/fixtures/hair_cuts.yml diff --git a/test/fixtures/iso_currencies.yml b/test/support/database/fixtures/iso_currencies.yml similarity index 100% rename from test/fixtures/iso_currencies.yml rename to test/support/database/fixtures/iso_currencies.yml diff --git a/test/fixtures/line_items.yml b/test/support/database/fixtures/line_items.yml similarity index 100% rename from test/fixtures/line_items.yml rename to test/support/database/fixtures/line_items.yml diff --git a/test/fixtures/makes.yml b/test/support/database/fixtures/makes.yml similarity index 100% rename from test/fixtures/makes.yml rename to test/support/database/fixtures/makes.yml diff --git a/test/fixtures/moons.yml b/test/support/database/fixtures/moons.yml similarity index 100% rename from test/fixtures/moons.yml rename to test/support/database/fixtures/moons.yml diff --git a/test/fixtures/numeros_telefone.yml b/test/support/database/fixtures/numeros_telefone.yml similarity index 100% rename from test/fixtures/numeros_telefone.yml rename to test/support/database/fixtures/numeros_telefone.yml diff --git a/test/fixtures/order_flags.yml b/test/support/database/fixtures/order_flags.yml similarity index 100% rename from test/fixtures/order_flags.yml rename to test/support/database/fixtures/order_flags.yml diff --git a/test/fixtures/patients.yml b/test/support/database/fixtures/patients.yml similarity index 100% rename from test/fixtures/patients.yml rename to test/support/database/fixtures/patients.yml diff --git a/test/fixtures/people.yml b/test/support/database/fixtures/people.yml similarity index 100% rename from test/fixtures/people.yml rename to test/support/database/fixtures/people.yml diff --git a/test/fixtures/pictures.yml b/test/support/database/fixtures/pictures.yml similarity index 100% rename from test/fixtures/pictures.yml rename to test/support/database/fixtures/pictures.yml diff --git a/test/fixtures/planet_types.yml b/test/support/database/fixtures/planet_types.yml similarity index 100% rename from test/fixtures/planet_types.yml rename to test/support/database/fixtures/planet_types.yml diff --git a/test/fixtures/planets.yml b/test/support/database/fixtures/planets.yml similarity index 100% rename from test/fixtures/planets.yml rename to test/support/database/fixtures/planets.yml diff --git a/test/fixtures/posts.yml b/test/support/database/fixtures/posts.yml similarity index 100% rename from test/fixtures/posts.yml rename to test/support/database/fixtures/posts.yml diff --git a/test/fixtures/posts_tags.yml b/test/support/database/fixtures/posts_tags.yml similarity index 100% rename from test/fixtures/posts_tags.yml rename to test/support/database/fixtures/posts_tags.yml diff --git a/test/fixtures/preferences.yml b/test/support/database/fixtures/preferences.yml similarity index 100% rename from test/fixtures/preferences.yml rename to test/support/database/fixtures/preferences.yml diff --git a/test/fixtures/products.yml b/test/support/database/fixtures/products.yml similarity index 100% rename from test/fixtures/products.yml rename to test/support/database/fixtures/products.yml diff --git a/test/fixtures/purchase_orders.yml b/test/support/database/fixtures/purchase_orders.yml similarity index 100% rename from test/fixtures/purchase_orders.yml rename to test/support/database/fixtures/purchase_orders.yml diff --git a/test/fixtures/questions.yml b/test/support/database/fixtures/questions.yml similarity index 100% rename from test/fixtures/questions.yml rename to test/support/database/fixtures/questions.yml diff --git a/test/fixtures/related_things.yml b/test/support/database/fixtures/related_things.yml similarity index 100% rename from test/fixtures/related_things.yml rename to test/support/database/fixtures/related_things.yml diff --git a/test/fixtures/sections.yml b/test/support/database/fixtures/sections.yml similarity index 100% rename from test/fixtures/sections.yml rename to test/support/database/fixtures/sections.yml diff --git a/test/fixtures/tags.yml b/test/support/database/fixtures/tags.yml similarity index 100% rename from test/fixtures/tags.yml rename to test/support/database/fixtures/tags.yml diff --git a/test/fixtures/things.yml b/test/support/database/fixtures/things.yml similarity index 100% rename from test/fixtures/things.yml rename to test/support/database/fixtures/things.yml diff --git a/test/fixtures/users.yml b/test/support/database/fixtures/users.yml similarity index 100% rename from test/fixtures/users.yml rename to test/support/database/fixtures/users.yml diff --git a/test/fixtures/vehicles.yml b/test/support/database/fixtures/vehicles.yml similarity index 100% rename from test/fixtures/vehicles.yml rename to test/support/database/fixtures/vehicles.yml diff --git a/test/fixtures/web_pages.yml b/test/support/database/fixtures/web_pages.yml similarity index 100% rename from test/fixtures/web_pages.yml rename to test/support/database/fixtures/web_pages.yml diff --git a/test/support/database/generator.rb b/test/support/database/generator.rb new file mode 100644 index 000000000..4e1b9f367 --- /dev/null +++ b/test/support/database/generator.rb @@ -0,0 +1,476 @@ +# In order to simplify testing of different ORMs and reduce differences in their schema +# generators, we use ActiveRecord::Schema to define our schema for readability and editing purposes. +# When running tests, all different ORMs (Sequel + ActiveRecord) will use the schema.sql to run +# their specs, ensuring a) a consistent test environment and b) easier adding of orms in the future. + +require 'active_support' +require 'active_record' +require 'yaml' + +ActiveSupport.eager_load! + +connection_spec = YAML.load_file(File.expand_path('../../database/config.yml', __FILE__))["test"] + +begin + ActiveRecord::Base.establish_connection(connection_spec) + + ActiveRecord::Schema.verbose = false + + puts "Loading schema into #{connection_spec["database"]}" + + ActiveRecord::Schema.define do + create_table :sessions, id: false, force: true do |t| + t.string :id, :limit => 36, :primary_key => true, null: false + t.string :survey_id, :limit => 36, null: false + + t.timestamps + end + + create_table :responses, force: true do |t| + #t.string :id, :limit => 36, :primary_key => true, null: false + + t.string :session_id, limit: 36, null: false + + t.string :type + t.string :question_id, limit: 36 + + t.timestamps + end + + create_table :response_texts, force: true do |t| + t.text :text + t.integer :response_id + + t.timestamps + end + + create_table :people, force: true do |t| + t.string :name + t.string :email + t.datetime :date_joined + t.belongs_to :preferences + t.integer :hair_cut_id, index: true + t.boolean :book_admin, default: false + t.boolean :special, default: false + t.timestamps null: false + end + + create_table :author_details, force: true do |t| + t.integer :person_id + t.string :author_stuff + t.timestamps null: false + end + + create_table :posts, force: true do |t| + t.string :title, length: 255 + t.text :body + t.integer :author_id + t.integer :parent_post_id + t.belongs_to :section, index: true + t.timestamps null: false + end + + create_table :comments, force: true do |t| + t.text :body + t.belongs_to :post, index: true + t.integer :author_id + t.timestamps null: false + end + + create_table :companies, force: true do |t| + t.string :type + t.string :name + t.string :address + t.timestamps null: false + end + + create_table :tags, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :sections, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :posts_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :posts_tags, [:post_id, :tag_id], unique: true + + create_table :special_post_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :special_post_tags, [:post_id, :tag_id], unique: true + + create_table :comments_tags, force: true do |t| + t.references :comment, :tag, index: true + end + + create_table :iso_currencies, id: false, force: true do |t| + t.string :code, limit: 3, null: false + t.string :name + t.string :country_name + t.string :minor_unit + t.timestamps null: false + end + add_index :iso_currencies, :code, unique: true + + create_table :expense_entries, force: true do |t| + t.string :currency_code, limit: 3, null: false + t.integer :employee_id, null: false + t.decimal :cost, precision: 12, scale: 4, null: false + t.date :transaction_date + t.timestamps null: false + end + + create_table :planets, force: true do |t| + t.string :name + t.string :description + t.integer :planet_type_id + end + + create_table :planets_tags, force: true do |t| + t.references :planet, :tag, index: true + end + add_index :planets_tags, [:planet_id, :tag_id], unique: true + + create_table :planet_types, force: true do |t| + t.string :name + end + + create_table :moons, force: true do |t| + t.string :name + t.string :description + t.integer :planet_id + t.timestamps null: false + end + + create_table :craters, id: false, force: true do |t| + t.string :code + t.string :description + t.integer :moon_id + t.timestamps null: false + end + + create_table :preferences, force: true do |t| + t.integer :person_id + t.boolean :advanced_mode, default: false + t.string :nickname + t.timestamps null: false + end + + create_table :facts, force: true do |t| + t.integer :person_id + t.string :spouse_name + t.text :bio + t.float :quality_rating + t.decimal :salary, precision: 12, scale: 2 + t.datetime :date_time_joined + t.date :birthday + t.time :bedtime + t.binary :photo, limit: 1.kilobyte + t.boolean :cool + t.timestamps null: false + end + + create_table :books, force: true do |t| + t.string :title + t.string :isbn + t.boolean :banned, default: false + t.timestamps null: false + end + + create_table :book_authors, force: true do |t| + t.integer :book_id + t.integer :person_id + end + + create_table :book_comments, force: true do |t| + t.text :body + t.belongs_to :book, index: true + t.integer :author_id + t.boolean :approved, default: true + t.timestamps null: false + end + + create_table :customers, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :purchase_orders, force: true do |t| + t.date :order_date + t.date :requested_delivery_date + t.date :delivery_date + t.integer :customer_id + t.string :delivery_name + t.string :delivery_address_1 + t.string :delivery_address_2 + t.string :delivery_city + t.string :delivery_state + t.string :delivery_postal_code + t.float :delivery_fee + t.float :tax + t.float :total + t.timestamps null: false + end + + create_table :order_flags, force: true do |t| + t.string :name + end + + create_table :purchase_orders_order_flags, force: true do |t| + t.references :purchase_order, :order_flag, index: true + end + add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" + + create_table :line_items, force: true do |t| + t.integer :purchase_order_id + t.string :part_number + t.string :quantity + t.float :item_cost + t.timestamps null: false + end + + create_table :hair_cuts, force: true do |t| + t.string :style + end + + create_table :numeros_telefone, force: true do |t| + t.string :numero_telefone + t.timestamps null: false + end + + create_table :categories, force: true do |t| + t.string :name + t.string :status, limit: 10 + t.timestamps null: false + end + + create_table :pictures, force: true do |t| + t.string :name + t.integer :author_id + t.references :imageable, polymorphic: true, index: true + t.timestamps null: false + end + + create_table :documents, force: true do |t| + t.string :name + t.integer :author_id + t.timestamps null: false + end + + create_table :products, force: true do |t| + t.string :name + t.integer :designer_id + t.timestamps null: false + end + + create_table :file_properties, force: true do |t| + t.string :name + #t.timestamps null: false + t.references :fileable, polymorphic: true, index: true + t.belongs_to :tag, index: true + + t.integer :size + end + + create_table :vehicles, force: true do |t| + t.string :type + t.string :make + t.string :model + t.string :length_at_water_line + t.string :drive_layout + t.string :serial_number + t.integer :person_id + t.timestamps null: false + end + + create_table :makes, force: true do |t| + t.string :model + t.timestamps null: false + end + + # special cases - fields that look like they should be reserved names + create_table :hrefs, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :links, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :web_pages, force: true do |t| + t.string :href + t.string :link + t.timestamps null: false + end + + create_table :questionables, force: true do |t| + t.timestamps null: false + end + + create_table :boxes, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :things, force: true do |t| + t.string :name + t.references :user + t.references :box + + t.timestamps null: false + end + + create_table :users, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :related_things, force: true do |t| + t.string :name + t.references :from, references: :thing + t.references :to, references: :thing + + t.timestamps null: false + end + + create_table :questions, force: true do |t| + t.string :text + t.timestamps null: false + end + + create_table :answers, force: true do |t| + t.references :question + t.integer :respondent_id + t.string :respondent_type + t.string :text + t.timestamps null: false + end + + create_table :patients, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :doctors, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :painters, force: true do |t| + t.string :name + + t.timestamps null: false + end + + create_table :paintings, force: true do |t| + t.string :title + t.string :category + t.belongs_to :painter + + t.timestamps null: false + end + + create_table :collectors, force: true do |t| + t.string :name + t.belongs_to :painting + end + + create_table :lists, force: true do |t| + t.string :name + end + + create_table :list_items, force: true do |t| + t.belongs_to :list + end + + # special cases + create_table :storages, force: true do |t| + t.string :token, null: false + t.string :name + t.timestamps null: false + end + + create_table :keepers, force: true do |t| + t.string :name + t.string :keepable_type, null: false + t.integer :keepable_id, null: false + t.timestamps null: false + end + + create_table :access_cards, force: true do |t| + t.string :token, null: false + t.string :security_level + t.timestamps null: false + end + + create_table :workers, force: true do |t| + t.string :name + t.integer :access_card_id, null: false + t.timestamps null: false + end + + create_table :agencies, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :indicators, force: true do |t| + t.string :name + t.string :import_id + t.integer :agency_id, null: false + t.timestamps null: false + end + + create_table :widgets, force: true do |t| + t.string :name + t.string :indicator_import_id, null: false + t.timestamps null: false + end + + create_table :robots, force: true do |t| + t.string :name + t.integer :version + t.timestamps null: false + end + end + + class FixtureGenerator + include ActiveRecord::TestFixtures + self.fixture_path = File.expand_path('../fixtures', __FILE__) + fixtures :all + + def self.load_fixtures + require_relative '../inflections' + require_relative '../active_record/models' + ActiveRecord::Base.connection.disable_referential_integrity do + FixtureGenerator.new.send(:load_fixtures, ActiveRecord::Base) + end + end + end + + puts "Loading fixture data into #{connection_spec["database"]}" + + FixtureGenerator.load_fixtures + + puts "Dumping data into data.sql" + + File.open(File.expand_path('../dump.sql', __FILE__), "w") do |f| + `sqlite3 test_db .tables`.split(/\s+/).each do |table_name| + f << %{DROP TABLE IF EXISTS "#{table_name}";\n} + puts "Dumping data from #{table_name}..." + f << `sqlite3 #{connection_spec["database"]} ".dump #{table_name}"` + end.join("\n") + f << "PRAGMA foreign_keys=ON;" # reenable foreign_keys + end + + puts "Done!" +ensure + File.delete(connection_spec["database"]) if File.exists?(connection_spec["database"]) +end diff --git a/test/support/inflections.rb b/test/support/inflections.rb new file mode 100644 index 000000000..6700d9393 --- /dev/null +++ b/test/support/inflections.rb @@ -0,0 +1,7 @@ +# These come from the model definitions and are required for fixture creation as well +# as test running. +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.uncountable 'preferences' + inflect.irregular 'numero_telefone', 'numeros_telefone' + inflect.uncountable 'file_properties' +end diff --git a/test/support/sequel/app_config.rb b/test/support/sequel/app_config.rb new file mode 100644 index 000000000..e69de29bb diff --git a/test/support/sequel/import_schema.rb b/test/support/sequel/import_schema.rb new file mode 100644 index 000000000..64542d629 --- /dev/null +++ b/test/support/sequel/import_schema.rb @@ -0,0 +1,6 @@ +statements = File.read(File.expand_path('../../database/dump.sql', __FILE__)).split(/;$/) +statements.pop # the last empty statement + +statements.each do |statement| + Sequel::Model.db[statement] +end diff --git a/test/support/sequel/initialize.rb b/test/support/sequel/initialize.rb new file mode 100644 index 000000000..48354f18b --- /dev/null +++ b/test/support/sequel/initialize.rb @@ -0,0 +1,2 @@ +require 'sequel_rails' +require 'sequel_rails/sequel/database/active_support_notification' \ No newline at end of file diff --git a/test/support/sequel/models.rb b/test/support/sequel/models.rb new file mode 100644 index 000000000..971da42b1 --- /dev/null +++ b/test/support/sequel/models.rb @@ -0,0 +1,370 @@ +require 'sequel' +require 'jsonapi-resources' + +config = Rails.configuration.database_configuration["test"] +config["adapter"] = "sqlite" if config["adapter"]=="sqlite3" +Sequel.connect(config) + +Sequel::Model.class_eval do + plugin :validation_class_methods + plugin :hook_class_methods + plugin :timestamps, update_on_create: true + plugin :single_table_inheritance, :type +end + +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.uncountable 'preferences' + inflect.irregular 'numero_telefone', 'numeros_telefone' +end + +### MODELS +class Person < Sequel::Model + one_to_many :posts, key: 'author_id' + one_to_many :comments, key: 'author_id' + one_to_many :expense_entries, key: 'employee_id', dependent: :restrict_with_exception + one_to_many :vehicles + many_to_one :preferences + many_to_one :hair_cut + one_to_one :author_detail + + many_to_many :books, join_table: :book_authors + + one_to_many :even_posts, conditions: 'posts.id % 2 = 0', class: 'Post', key: 'author_id' + one_to_many :odd_posts, conditions: 'posts.id % 2 = 1', class: 'Post', key: 'author_id' + + ### Validations + validates_presence_of :name, :date_joined +end + +class AuthorDetail < Sequel::Model + many_to_one :author, class: 'Person', key: 'person_id' +end + +class Post < Sequel::Model + many_to_one :author, class: 'Person', key: 'author_id' + many_to_one :writer, class: 'Person', key: 'author_id' + one_to_many :comments + many_to_many :tags, join_table: :posts_tags + one_to_many :special_post_tags, source: :tag + one_to_many :special_tags, through: :special_post_tags, source: :tag + many_to_one :section + one_to_one :parent_post, class: 'Post', key: 'parent_post_id' + + validates_presence_of :author + validates_length_of :title, maximum: 35 + + before_destroy :destroy_callback + + def destroy_callback + if title == "can't destroy me" + errors.add(:title, "can't destroy me") + + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class SpecialPostTag < Sequel::Model + many_to_one :tag + many_to_one :post +end + +class Comment < Sequel::Model + many_to_one :author, class: 'Person', key: 'author_id' + many_to_one :post + many_to_many :tags, join_table: :comments_tags +end + +class Company < Sequel::Model +end + +class Firm < Company +end + +class Tag < Sequel::Model + many_to_many :posts, join_table: :posts_tags + many_to_many :planets, join_table: :planets_tags +end + +class Section < Sequel::Model + one_to_many :posts +end + +class HairCut < Sequel::Model + one_to_many :people +end + +class Property < Sequel::Model +end + +class Customer < Sequel::Model +end + +class BadlyNamedAttributes < Sequel::Model +end + +class Cat < Sequel::Model +end + +class IsoCurrency < Sequel::Model + set_primary_key :code + # one_to_many :expense_entries, key: 'currency_code' +end + +class ExpenseEntry < Sequel::Model + many_to_one :employee, class: 'Person', key: 'employee_id' + many_to_one :iso_currency, key: 'currency_code' +end + +class Planet < Sequel::Model + one_to_many :moons + many_to_one :planet_type + + many_to_many :tags, join_table: :planets_tags + + # Test model callback cancelling save + before_save :check_not_pluto + + def check_not_pluto + # Pluto can't be a planet, so cancel the save + if name.downcase == 'pluto' + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class PlanetType < Sequel::Model + one_to_many :planets +end + +class Moon < Sequel::Model + many_to_one :planet + + one_to_many :craters +end + +class Crater < Sequel::Model + set_primary_key :code + + many_to_one :moon +end + +class Preferences < Sequel::Model + one_to_one :author, class: 'Person', :inverse_of => 'preferences' +end + +class Fact < Sequel::Model + validates_presence_of :spouse_name, :bio +end + +class Like < Sequel::Model +end + +class Breed + + def initialize(id = nil, name = nil) + if id.nil? + @id = $breed_data.new_id + $breed_data.add(self) + else + @id = id + end + @name = name + @errors = Sequel::Model::Errors.new + end + + attr_accessor :id, :name + + def destroy + $breed_data.remove(@id) + end + + def valid?(context = nil) + @errors.clear + if name.is_a?(String) && name.length > 0 + return true + else + @errors.add(:name, "can't be blank") + return false + end + end + + def errors + @errors + end +end + +class Book < Sequel::Model + one_to_many :book_comments + one_to_many :approved_book_comments, conditions: {approved: true}, class: "BookComment" + + many_to_many :authors, join_table: :book_authors, class: "Person" +end + +class BookComment < Sequel::Model + many_to_one :author, class: 'Person', key: 'author_id' + many_to_one :book + + def before_save + debugger + end + + def self.for_user(current_user) + records = self + # Hide the unapproved comments from people who are not book admins + unless current_user && current_user.book_admin + records = records.where(approved: true) + end + records + end +end + +class BreedData + def initialize + @breeds = {} + end + + def breeds + @breeds + end + + def new_id + @breeds.keys.max + 1 + end + + def add(breed) + @breeds[breed.id] = breed + end + + def remove(id) + @breeds.delete(id) + end +end + +class Customer < Sequel::Model + one_to_many :purchase_orders +end + +class PurchaseOrder < Sequel::Model + many_to_one :customer + one_to_many :line_items + one_to_many :admin_line_items, class: 'LineItem', key: 'purchase_order_id' + + many_to_many :order_flags, join_table: :purchase_orders_order_flags + + many_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class: 'OrderFlag' +end + +class OrderFlag < Sequel::Model + many_to_many :purchase_orders, join_table: :purchase_orders_order_flags +end + +class LineItem < Sequel::Model + many_to_one :purchase_order +end + +class NumeroTelefone < Sequel::Model +end + +class Category < Sequel::Model +end + +class Picture < Sequel::Model + many_to_one :imageable, polymorphic: true +end + +class Vehicle < Sequel::Model + many_to_one :person +end + +class Car < Vehicle +end + +class Boat < Vehicle +end + +class Document < Sequel::Model + one_to_many :pictures, as: :imageable +end + +class Document::Topic < Document +end + +class Product < Sequel::Model + one_to_one :picture, as: :imageable +end + +class Make < Sequel::Model +end + +class WebPage < Sequel::Model +end + +class Box < Sequel::Model + one_to_many :things +end + +class User < Sequel::Model + one_to_many :things +end + +class Thing < Sequel::Model + many_to_one :box + many_to_one :user + + one_to_many :related_things, key: :from_id + one_to_many :things, through: :related_things, source: :to +end + +class RelatedThing < Sequel::Model + many_to_one :from, class: Thing, key: :from_id + many_to_one :to, class: Thing, key: :to_id +end + +class Question < Sequel::Model + one_to_one :answer + + def respondent + answer.try(:respondent) + end +end + +class Answer < Sequel::Model + many_to_one :question + many_to_one :respondent, polymorphic: true +end + +class Patient < Sequel::Model +end + +class Doctor < Sequel::Model +end + +module Api + module V7 + class Client < Customer + end + + class Customer < Customer + end + end +end + +### PORO Data - don't do this in a production app +$breed_data = BreedData.new +$breed_data.add(Breed.new(0, 'persian')) +$breed_data.add(Breed.new(1, 'siamese')) +$breed_data.add(Breed.new(2, 'sphinx')) +$breed_data.add(Breed.new(3, 'to_delete')) diff --git a/test/support/sequel/rollback.rb b/test/support/sequel/rollback.rb new file mode 100644 index 000000000..914855caf --- /dev/null +++ b/test/support/sequel/rollback.rb @@ -0,0 +1,22 @@ +module Minitest + module Rollback + + def before_setup + Sequel::Model.db.synchronize do |conn| + Sequel::Model.db.send(:add_transaction, conn, {}) + Sequel::Model.db.send(:begin_transaction, conn) + end + super + end + + def after_teardown + super + Sequel::Model.db.synchronize {|conn| Sequel::Model.db.send(:rollback_transaction, conn) } + end + + end + + class Test + include Rollback + end +end diff --git a/test/support/sequel/setup.rb b/test/support/sequel/setup.rb new file mode 100644 index 000000000..98eb3921b --- /dev/null +++ b/test/support/sequel/setup.rb @@ -0,0 +1,9 @@ +TestApp.class_eval do + config.active_record.schema_format = :none + + if Rails::VERSION::MAJOR >= 5 + config.active_support.halt_callback_chains_on_return_false = false + config.active_record.time_zone_aware_types = [:time, :datetime] + config.active_record.belongs_to_required_by_default = false + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index b8e6acc40..80a51243a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,5 +1,5 @@ -require 'simplecov' require 'database_cleaner' +require 'simplecov' # To run tests with coverage: # COVERAGE=true bundle exec rake test @@ -21,7 +21,10 @@ end end -require 'active_record/railtie' +ENV["ORM"] = "active_record" + +require_relative "support/#{ENV["ORM"]}/initialize" +require_relative "support/inflections" require 'rails/test_help' require 'minitest/mock' require 'jsonapi-resources' @@ -43,7 +46,7 @@ ActiveSupport::Deprecation.silenced = true -puts "Testing With RAILS VERSION #{Rails.version}" +puts "Testing With RAILS VERSION #{Rails.version} and #{ENV["ORM"]} ORM" class TestApp < Rails::Application config.eager_load = false @@ -54,18 +57,11 @@ class TestApp < Rails::Application #Raise errors on unsupported parameters config.action_controller.action_on_unpermitted_parameters = :raise - ActiveRecord::Schema.verbose = false - config.active_record.schema_format = :none + config.paths["config/database"] = "support/database/config.yml" + config.active_support.test_order = :random - if Rails::VERSION::MAJOR >= 5 - config.active_support.halt_callback_chains_on_return_false = false - config.active_record.time_zone_aware_types = [:time, :datetime] - config.active_record.belongs_to_required_by_default = false - if Rails::VERSION::MINOR >= 2 - config.active_record.sqlite3.represent_boolean_as_integer = true - end - end + ActiveSupport::Deprecation.silenced = true end module MyEngine @@ -200,7 +196,7 @@ def assert_query_count(expected, msg = nil, &block) callback = lambda {|_, _, _, _, payload| @queries.push payload[:sql] } - ActiveSupport::Notifications.subscribed(callback, 'sql.active_record', &block) + ActiveSupport::Notifications.subscribed(callback, "sql.#{ENV["ORM"]}", &block) show_queries unless expected == @queries.size assert expected == @queries.size, "Expected #{expected} queries, ran #{@queries.size} queries" @@ -226,7 +222,16 @@ def show_queries TestApp.initialize! -require File.expand_path('../fixtures/active_record', __FILE__) +require_relative "support/#{ENV["ORM"]}/app_config" + +# We used to have the schema in the ActiveRecord schema creation format, but then we would need +# to reimplement the schema bulider in other ORMs that we are testing. We could always require ActiveRecord +# for the purposes of schema creation, but then we would have to try to remove ActiveRecord from the global +# namespace to really have no side-effects when running the specs. The goal of running the specs with other +# orms is to not have ActiveRecord required in at all. +require_relative "support/#{ENV["ORM"]}/import_schema" +require_relative "support/#{ENV["ORM"]}/models" +require_relative "support/controllers_resources_processors" module Pets module V1 @@ -487,27 +492,19 @@ class Minitest::Test include Helpers::ValueMatchers include Helpers::FunctionalHelpers include Helpers::ConfigurationHelpers - include ActiveRecord::TestFixtures def run_in_transaction? true end - - self.fixture_path = "#{Rails.root}/fixtures" - fixtures :all end class ActiveSupport::TestCase - self.fixture_path = "#{Rails.root}/fixtures" - fixtures :all setup do @routes = TestApp.routes end end class ActionDispatch::IntegrationTest - self.fixture_path = "#{Rails.root}/fixtures" - fixtures :all def assert_jsonapi_response(expected_status, msg = nil) assert_equal JSONAPI::MEDIA_TYPE, response.content_type @@ -562,7 +559,7 @@ def assert_cacheable_get(action, *args) normal_queries = [] normal_query_callback = lambda {|_, _, _, _, payload| normal_queries.push payload[:sql] } - ActiveSupport::Notifications.subscribed(normal_query_callback, 'sql.active_record') do + ActiveSupport::Notifications.subscribed(normal_query_callback, "sql.#{ENV["ORM"]}") do get action, *args end non_caching_response = json_response_sans_all_backtraces @@ -595,7 +592,7 @@ def assert_cacheable_get(action, *args) cache_queries.push payload[:sql] } cache_activity[phase] = with_resource_caching(cache, cached_resources) do - ActiveSupport::Notifications.subscribed(cache_query_callback, 'sql.active_record') do + ActiveSupport::Notifications.subscribed(cache_query_callback, "sql.#{ENV["ORM"]}") do @controller = nil setup_controller_request_and_response @request.headers.merge!(orig_request_headers.dup) @@ -735,3 +732,5 @@ def unformat(formatted_route) end end end + +require_relative "support/#{ENV["ORM"]}/setup" diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index 0bb8d1aa7..770abde17 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -431,8 +431,7 @@ def test_key_type_proc end def test_id_attr_deprecation - - ActiveSupport::Deprecation.silenced = false + tmp, ActiveSupport::Deprecation.silenced = ActiveSupport::Deprecation.silenced, false _out, err = capture_io do eval <<-CODE class ProblemResource < JSONAPI::Resource @@ -442,7 +441,7 @@ class ProblemResource < JSONAPI::Resource end assert_match /DEPRECATION WARNING: Id without format is no longer supported. Please remove ids from attributes, or specify a format./, err ensure - ActiveSupport::Deprecation.silenced = true + ActiveSupport::Deprecation.silenced = tmp end def test_id_attr_with_format