active_model_serializers/test/cache_test.rb
kevintyll ab6bd600e3 When caching, return the object's cache_key up front if it's defined.
This will prevent objects PORO objects that don't have updated_at defined, from throwing an error.

Not as big a deal now that PORO objects can inherit ActiveModelSerializers::Model, but still necessary if it's not inherited for whatever reason.

Add the Adapter type to the cache key.

This prevents incorrect results when the same object is serialized with different adapters.

BF:

Cherry-pick of
040a97b9e9
which was a squash of
f89ed71058

from pr 1346
2016-03-31 22:29:13 -05:00

311 lines
12 KiB
Ruby

require 'test_helper'
require 'tmpdir'
require 'tempfile'
module ActiveModelSerializers
class CacheTest < ActiveSupport::TestCase
UncachedAuthor = Class.new(Author) do
# To confirm cache_key is set using updated_at and cache_key option passed to cache
undef_method :cache_key
end
Article = Class.new(::Model) do
# To confirm error is raised when cache_key is not set and cache_key option not passed to cache
undef_method :cache_key
end
ArticleSerializer = Class.new(ActiveModel::Serializer) do
cache only: [:place], skip_digest: true
attributes :title
end
InheritedRoleSerializer = Class.new(RoleSerializer) do
cache key: 'inherited_role', only: [:name, :special_attribute]
attribute :special_attribute
end
setup do
cache_store.clear
@comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
@post = Post.new(title: 'New Post', body: 'Body')
@bio = Bio.new(id: 1, content: 'AMS Contributor')
@author = Author.new(name: 'Joao M. D. Moura')
@blog = Blog.new(id: 999, name: 'Custom blog', writer: @author, articles: [])
@role = Role.new(name: 'Great Author')
@location = Location.new(lat: '-23.550520', lng: '-46.633309')
@place = Place.new(name: 'Amazing Place')
@author.posts = [@post]
@author.roles = [@role]
@role.author = @author
@author.bio = @bio
@bio.author = @author
@post.comments = [@comment]
@post.author = @author
@comment.post = @post
@comment.author = @author
@post.blog = @blog
@location.place = @place
@location_serializer = LocationSerializer.new(@location)
@bio_serializer = BioSerializer.new(@bio)
@role_serializer = RoleSerializer.new(@role)
@post_serializer = PostSerializer.new(@post)
@author_serializer = AuthorSerializer.new(@author)
@comment_serializer = CommentSerializer.new(@comment)
@blog_serializer = BlogSerializer.new(@blog)
end
def test_explicit_cache_store
default_store = Class.new(ActiveModel::Serializer) do
cache
end
explicit_store = Class.new(ActiveModel::Serializer) do
cache cache_store: ActiveSupport::Cache::FileStore
end
assert ActiveSupport::Cache::MemoryStore, ActiveModelSerializers.config.cache_store
assert ActiveSupport::Cache::MemoryStore, default_store.cache_store
assert ActiveSupport::Cache::FileStore, explicit_store.cache_store
end
def test_inherited_cache_configuration
inherited_serializer = Class.new(PostSerializer)
assert_equal PostSerializer._cache_key, inherited_serializer._cache_key
assert_equal PostSerializer._cache_options, inherited_serializer._cache_options
end
def test_override_cache_configuration
inherited_serializer = Class.new(PostSerializer) do
cache key: 'new-key'
end
assert_equal PostSerializer._cache_key, 'post'
assert_equal inherited_serializer._cache_key, 'new-key'
end
def test_cache_definition
assert_equal(cache_store, @post_serializer.class._cache)
assert_equal(cache_store, @author_serializer.class._cache)
assert_equal(cache_store, @comment_serializer.class._cache)
end
def test_cache_key_definition
assert_equal('post', @post_serializer.class._cache_key)
assert_equal('writer', @author_serializer.class._cache_key)
assert_equal(nil, @comment_serializer.class._cache_key)
end
def test_error_is_raised_if_cache_key_is_not_defined_on_object_or_passed_as_cache_option
article = Article.new(title: 'Must Read')
assert_raises ActiveModel::Serializer::Adapter::CachedSerializer::UndefinedCacheKey do
render_object_with_cache(article)
end
end
def test_cache_key_interpolation_with_updated_at_when_cache_key_is_not_defined_on_object
uncached_author = UncachedAuthor.new(name: 'Joao M. D. Moura')
uncached_author_serializer = AuthorSerializer.new(uncached_author)
render_object_with_cache(uncached_author)
key = cache_key_with_adapter("#{uncached_author_serializer.class._cache_key}/#{uncached_author_serializer.object.id}-#{uncached_author_serializer.object.updated_at.strftime("%Y%m%d%H%M%S%9N")}")
assert_equal(uncached_author_serializer.attributes.to_json, ActionController::Base.cache_store.fetch(key).to_json)
end
def test_default_cache_key_fallback
render_object_with_cache(@comment)
assert_equal(@comment_serializer.attributes.to_json, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@comment.cache_key)).to_json)
end
def test_cache_options_definition
assert_equal({ expires_in: 0.1, skip_digest: true }, @post_serializer.class._cache_options)
assert_equal(nil, @blog_serializer.class._cache_options)
assert_equal({ expires_in: 1.day, skip_digest: true }, @comment_serializer.class._cache_options)
end
def test_fragment_cache_definition
assert_equal([:name], @role_serializer.class._cache_only)
assert_equal([:content], @bio_serializer.class._cache_except)
end
def test_associations_separately_cache
cache_store.clear
assert_equal(nil, cache_store.fetch(@post.cache_key))
assert_equal(nil, cache_store.fetch(@comment.cache_key))
Timecop.freeze(Time.current) do
render_object_with_cache(@post)
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@post.cache_key)))
assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@comment.cache_key)))
end
end
def test_associations_cache_when_updated
Timecop.freeze(Time.current) do
# Generate a new Cache of Post object and each objects related to it.
render_object_with_cache(@post)
# Check if it cached the objects separately
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@post.cache_key)))
assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@comment.cache_key)))
# Simulating update on comments relationship with Post
new_comment = Comment.new(id: 2567, body: 'ZOMG A NEW COMMENT')
new_comment_serializer = CommentSerializer.new(new_comment)
@post.comments = [new_comment]
# Ask for the serialized object
render_object_with_cache(@post)
# Check if the the new comment was cached
assert_equal(new_comment_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(new_comment.cache_key)))
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@post.cache_key)))
end
end
def test_fragment_fetch_with_virtual_associations
expected_result = {
id: @location.id,
lat: @location.lat,
lng: @location.lng,
place: 'Nowhere'
}
hash = render_object_with_cache(@location)
assert_equal(hash, expected_result)
assert_equal({ place: 'Nowhere' }, ActionController::Base.cache_store.fetch(cache_key_with_adapter(@location.cache_key)))
end
def test_fragment_cache_with_inheritance
inherited = render_object_with_cache(@role, serializer: InheritedRoleSerializer)
base = render_object_with_cache(@role)
assert_includes(inherited.keys, :special_attribute)
refute_includes(base.keys, :special_attribute)
end
def test_uses_adapter_in_cache_key
render_object_with_cache(@post)
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch("#{@post.cache_key}/#{adapter.class.to_s.demodulize.underscore}"))
end
def test_uses_file_digest_in_cache_key
render_object_with_cache(@blog)
assert_equal(@blog_serializer.attributes, ActionController::Base.cache_store.fetch("#{cache_key_with_adapter(@blog.cache_key)}/#{@blog.class::FILE_DIGEST}"))
end
def test_cache_digest_definition
assert_equal(FILE_DIGEST, @post_serializer.class._cache_digest)
end
def test_object_cache_keys
serializer = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment])
include_tree = ActiveModel::Serializer::IncludeTree.from_include_args('*')
actual = CachedSerializer.object_cache_keys(serializer, include_tree)
assert_equal actual.size, 3
assert actual.any? { |key| key == 'comment/1' }
assert actual.any? { |key| key =~ %r{post/post-\d+} }
assert actual.any? { |key| key =~ %r{writer/author-\d+} }
end
def test_cached_attributes
serializer = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment])
Timecop.freeze(Time.current) do
render_object_with_cache(@comment)
attributes = Adapter::Attributes.new(serializer)
attributes.send(:cache_attributes)
cached_attributes = attributes.instance_variable_get(:@cached_attributes)
assert_equal cached_attributes[@comment.cache_key], Comment.new(id: 1, body: 'ZOMG A COMMENT').attributes
assert_equal cached_attributes[@comment.post.cache_key], Post.new(id: 'post', title: 'New Post', body: 'Body').attributes
writer = @comment.post.blog.writer
writer_cache_key = "writer/#{writer.id}-#{writer.updated_at.strftime("%Y%m%d%H%M%S%9N")}"
assert_equal cached_attributes[writer_cache_key], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes
end
end
def test_serializer_file_path_on_nix
path = '/Users/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb'
caller_line = "#{path}:1:in `<top (required)>'"
assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
end
def test_serializer_file_path_on_windows
path = 'c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb'
caller_line = "#{path}:1:in `<top (required)>'"
assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
end
def test_serializer_file_path_with_space
path = '/Users/git/ember js/ember-crm-backend/app/serializers/lead_serializer.rb'
caller_line = "#{path}:1:in `<top (required)>'"
assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
end
def test_serializer_file_path_with_submatch
# The submatch in the path ensures we're using a correctly greedy regexp.
path = '/Users/git/ember js/ember:123:in x/app/serializers/lead_serializer.rb'
caller_line = "#{path}:1:in `<top (required)>'"
assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
end
def test_digest_caller_file
contents = "puts 'AMS rocks'!"
dir = Dir.mktmpdir('space char')
file = Tempfile.new('some_ruby.rb', dir)
file.write(contents)
path = file.path
caller_line = "#{path}:1:in `<top (required)>'"
file.close
assert_equal ActiveModel::Serializer.digest_caller_file(caller_line), Digest::MD5.hexdigest(contents)
ensure
file.unlink
FileUtils.remove_entry dir
end
def test_warn_on_serializer_not_defined_in_file
called = false
serializer = Class.new(ActiveModel::Serializer)
assert_output(nil, /_cache_digest/) do
serializer.digest_caller_file('')
called = true
end
assert called
end
private
def render_object_with_cache(obj, options = {})
@serializable_resource = serializable(obj, options).serializable_hash
@serializable_resource.serializable_hash
end
def adapter
@serializable_resource.adapter
end
def cache_key_with_adapter(key)
"#{key}/#{adapter.name.underscore}"
end
def cache_store
ActiveModelSerializers.config.cache_store
end
def cached_serialization(serializer)
cache_key = CachedSerializer.new(serializer).cache_key
cache_store.fetch(cache_key)
end
end
end