diff --git a/test/test_orm_serialization.rb b/test/test_orm_serialization.rb new file mode 100644 index 0000000..eb6c784 --- /dev/null +++ b/test/test_orm_serialization.rb @@ -0,0 +1,145 @@ +require 'minitest/autorun' +require 'json' +require 'prism' +require 'tmpdir' + +class OrmSerializationTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-serialization-store') + @store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore')) + @store.create_table(:authors, %w[id name email password_digest created_at]) + @store.create_table(:articles, %w[id author_id title body]) + + author_class = Class.new(Prism::Orm::Model) do + table_name :authors + has_many :articles, class_name: 'Article', foreign_key: 'author_id' + has_secure_password validations: false + + def greeting + "hi #{name}" + end + end + article_class = Class.new(Prism::Orm::Model) do + table_name :articles + belongs_to :author, class_name: 'Author', foreign_key: 'author_id' + end + [author_class, article_class].each { |klass| klass.store = @store } + Object.const_set(:Author, author_class) + Object.const_set(:Article, article_class) + end + + def teardown + FileUtils.remove_entry(@dir) + %i[Author Article].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) } + end + + def test_serializable_hash_returns_all_attributes_by_default + author = Author.create!(name: 'Ada', email: 'ada@example.com') + hash = author.serializable_hash + assert_equal 'Ada', hash['name'] + assert_equal 'ada@example.com', hash['email'] + assert_equal author.id, hash['id'] + end + + def test_only_filters_to_named_attributes + author = Author.create!(name: 'Ada', email: 'ada@example.com') + hash = author.serializable_hash(only: :name) + assert_equal({ 'name' => 'Ada' }, hash) + end + + def test_except_removes_named_attributes + author = Author.create!(name: 'Ada', email: 'ada@example.com') + hash = author.serializable_hash(except: [:email, :created_at, :updated_at]) + assert_equal %w[id name], hash.keys.sort + end + + def test_methods_option_adds_computed_values + author = Author.create!(name: 'Ada', email: 'ada@example.com') + hash = author.serializable_hash(only: :name, methods: :greeting) + assert_equal 'hi Ada', hash['greeting'] + end + + def test_password_digest_is_hidden_from_json_by_default + author = Author.create!(name: 'Ada', email: 'ada@example.com', password: 'sekret123') + refute_nil author['password_digest'] + hash = author.serializable_hash + refute hash.key?('password_digest'), 'password_digest must not leak through default serialization' + end + + def test_explicit_only_can_override_the_default_hide + author = Author.create!(name: 'Ada', email: 'ada@example.com', password: 'sekret123') + hash = author.serializable_hash(only: %i[name password_digest]) + assert hash.key?('password_digest'), 'an explicit only: is a deliberate opt-in, it must win' + end + + def test_as_json_stringifies_time_columns + author = Author.create!(name: 'Ada', email: 'ada@example.com') + assert_kind_of Time, author['created_at'], 'sanity check: raw attribute is a Time' + json = author.as_json + assert_kind_of String, json['created_at'] + assert_equal author['created_at'].iso8601, json['created_at'] + end + + def test_to_json_round_trips_through_json_generate + author = Author.create!(name: 'Ada', email: 'ada@example.com') + parsed = JSON.parse(author.to_json) + assert_equal 'Ada', parsed['name'] + refute parsed.key?('password_digest') + end + + def test_json_generate_on_a_bare_record_uses_to_json + author = Author.create!(name: 'Ada', email: 'ada@example.com') + parsed = JSON.parse(JSON.generate(author)) + assert_equal 'Ada', parsed['name'] + end + + def test_include_serializes_a_belongs_to_association + author = Author.create!(name: 'Ada', email: 'ada@example.com') + article = Article.create!(author_id: author.id, title: 'Hello', body: 'World') + hash = article.serializable_hash(include: :author) + assert_equal 'Ada', hash['author']['name'] + end + + def test_include_serializes_a_has_many_collection + author = Author.create!(name: 'Ada', email: 'ada@example.com') + Article.create!(author_id: author.id, title: 'One', body: 'x') + Article.create!(author_id: author.id, title: 'Two', body: 'y') + hash = author.serializable_hash(only: :name, include: { articles: { only: :title } }) + titles = hash['articles'].map { |row| row['title'] } + assert_equal %w[One Two], titles.sort + end + + def test_include_with_nil_belongs_to_yields_nil + orphan = Article.create!(author_id: nil, title: 'Orphan', body: 'x') + hash = orphan.serializable_hash(include: :author) + assert_nil hash['author'] + end + + def test_relation_as_json_serializes_each_record + Author.create!(name: 'Ada', email: 'a@example.com') + Author.create!(name: 'Bea', email: 'b@example.com') + rows = Author.all.as_json(only: :name) + assert_equal [{ 'name' => 'Ada' }, { 'name' => 'Bea' }], rows.sort_by { |row| row['name'] } + end + + def test_relation_to_json_produces_a_json_array_without_leaking_hidden_columns + Author.create!(name: 'Ada', email: 'a@example.com', password: 'sekret123') + parsed = JSON.parse(Author.all.to_json) + assert_equal 1, parsed.size + refute parsed.first.key?('password_digest') + end + + def test_hide_from_json_is_inherited_by_subclasses + base_class = Class.new(Prism::Orm::Model) { table_name :authors } + base_class.hide_from_json(:email) + Object.const_set(:BaseAuthor, base_class) + sub_class = Class.new(base_class) { table_name :authors } + Object.const_set(:SubAuthor, sub_class) + sub_class.store = @store + + record = sub_class.create!(name: 'Ada', email: 'ada@example.com') + refute record.serializable_hash.key?('email') + ensure + %i[BaseAuthor SubAuthor].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) } + end +end diff --git a/test/test_orm_store.rb b/test/test_orm_store.rb index 1384817..5d5c704 100644 --- a/test/test_orm_store.rb +++ b/test/test_orm_store.rb @@ -104,6 +104,52 @@ class OrmStoreTest < Minitest::Test assert_empty @store.applied_migrations end + def test_insert_emits_sql_prism_event + @store.create_table(:users, %w[id name]) + events = [] + subscriber = Prism::Instrumentation.subscribe('sql.prism') { |event| events << event } + begin + @store.insert(:users, name: 'foksiu') + ensure + Prism::Instrumentation.unsubscribe(subscriber) + end + + assert_equal 1, events.size + assert_equal 'INSERT', events.first.payload[:operation] + assert_equal 'users', events.first.payload[:table] + end + + def test_find_and_all_and_count_emit_select_events + @store.create_table(:users, %w[id name]) + id = @store.insert(:users, name: 'foksiu') + events = [] + subscriber = Prism::Instrumentation.subscribe('sql.prism') { |event| events << event } + begin + @store.find(:users, id) + @store.all(:users) + @store.count(:users) + ensure + Prism::Instrumentation.unsubscribe(subscriber) + end + + assert_equal %w[SELECT SELECT], events.first(2).map { |e| e.payload[:operation] } + assert_equal 'SELECT COUNT', events.last.payload[:operation] + end + + def test_update_missing_record_still_emits_event_with_exception + @store.create_table(:users, %w[id name]) + events = [] + subscriber = Prism::Instrumentation.subscribe('sql.prism') { |event| events << event } + begin + assert_raises(Prism::Orm::Store::RecordNotFound) { @store.update(:users, 999, name: 'nope') } + ensure + Prism::Instrumentation.unsubscribe(subscriber) + end + + assert_equal 1, events.size + assert_equal 'Prism::Orm::Store::RecordNotFound', events.first.payload[:exception].first + end + def test_data_survives_reopening_the_store @store.create_table(:users, %w[id name]) @store.insert(:users, name: 'persisted-foksiu')