test: cover PreviewFixtures and PreviewPost

This commit is contained in:
dualfroz committed 2026-09-10 19:15:57 +02:00
1 parent 3764cd9822
commit 726e1cd908
1 file changed
+128
+128
View File
@@ -0,0 +1,128 @@
require 'minitest/autorun'
require 'prism'
require 'tmpdir'
# Fixtures built once at file-load time (same shared-once idiom as
# TransactionalFixtures in test_testing_transactional.rb) instead of the
# per-test Dir.mktmpdir/FileUtils.remove_entry setup/teardown every other
# *_orm test file repeats - safe here because `use_transactional_tests`
# rolls each test's writes back, so nothing needs re-seeding between runs.
#
# PreviewApp mirrors PostsController#preview/#update from examples/blog
# closely enough to exercise the same assign -> valid? -> changes ->
# restore_attributes sequence, and the same optimistic-locking
# StaleObjectError path, through a REAL HTTP request cycle - something only
# ever exercised end-to-end via blog_demo.rb until now. It does not require
# examples/blog/app.rb itself: that file calls `Prism::Orm.connect` against
# a real on-disk dev database as a process-wide global, which would leak
# into every other test in this suite that relies on `Prism::Orm.store`.
module PreviewFixtures
DIR = Dir.mktmpdir('prism-preview-integration')
STORE = Prism::Orm::Store.new(File.join(DIR, 'test.pstore'))
STORE.create_table(:preview_posts, %w[id title body lock_version])
class PreviewPost < Prism::Orm::Model
self.store = STORE
table_name :preview_posts
validates :title, presence: true
end
class PreviewApp < Prism::Application
get('/posts/:id') do
post = PreviewPost.find(params['id'])
json(id: post.id, title: post.title, body: post.body, lock_version: post.lock_version)
end
# Same shape as PostsController#preview: dirty-track a proposed edit,
# capture the diff and any validation errors, then restore the record
# to exactly what was persisted before responding - nothing here ever
# calls #save.
patch('/posts/:id/preview') do
post = PreviewPost.find(params['id'])
attempted = params.require('post').permit(:title, :body, :lock_version).to_h
post.assign(attempted)
post.valid?
diff = post.changes.reject { |field, _| field == 'lock_version' }
errors = post.errors.full_messages
post.restore_attributes
json(diff: diff, errors: errors, current_title: post.title, current_body: post.body)
end
patch('/posts/:id') do
post = PreviewPost.find(params['id'])
post.assign(params.require('post').permit(:title, :body, :lock_version).to_h)
begin
post.save!
json(id: post.id, title: post.title, lock_version: post.lock_version)
rescue Prism::Orm::StaleObjectError
halt(409, 'stale object')
end
end
end
end
class IntegrationPreviewTest < Minitest::Test
include Prism::Testing::IntegrationTest
integration_app PreviewFixtures::PreviewApp
use_transactional_tests(store: PreviewFixtures::STORE)
def test_preview_returns_a_diff_without_persisting_the_change
post = PreviewFixtures::PreviewPost.create!(title: 'Original title', body: 'original body')
patch("/posts/#{post.id}/preview", params: { post: { title: 'Proposed title', body: 'original body' } })
assert_response :success
body = response.json
assert_equal ['Original title', 'Proposed title'], body.dig('diff', 'title')
assert_empty body['errors']
assert_equal 'Original title', body['current_title'], 'the response must show the record as still unsaved'
get("/posts/#{post.id}")
assert_equal 'Original title', response.json['title'], 'the store itself must be untouched by the preview'
end
def test_preview_reports_validation_errors_without_persisting_anything
post = PreviewFixtures::PreviewPost.create!(title: 'Keep me', body: 'keep me too')
patch("/posts/#{post.id}/preview", params: { post: { title: '', body: 'still keep me too' } })
assert_response :success
body = response.json
refute_empty body['errors']
assert_equal 'Keep me', body['current_title']
get("/posts/#{post.id}")
assert_equal 'Keep me', response.json['title']
end
def test_preview_is_unaffected_by_a_stale_lock_version_because_it_never_saves
post = PreviewFixtures::PreviewPost.create!(title: 'Racing edit', body: 'body')
stale_lock_version = post.lock_version
post.update!(body: 'a concurrent edit landed first')
refute_equal stale_lock_version, post.lock_version
patch("/posts/#{post.id}/preview",
params: { post: { title: 'Whatever I typed', body: 'body', lock_version: stale_lock_version } })
assert_response :success # must not raise StaleObjectError - it only diffs, it never saves
assert_equal 'a concurrent edit landed first', response.json['current_body']
end
def test_real_update_with_a_stale_lock_version_is_rejected_with_409
post = PreviewFixtures::PreviewPost.create!(title: 'Racing edit', body: 'body')
stale_lock_version = post.lock_version
post.update!(body: 'a concurrent edit landed first')
patch("/posts/#{post.id}", params: { post: { title: 'Racing edit', body: 'my edit', lock_version: stale_lock_version } })
assert_response 409
get("/posts/#{post.id}")
assert_equal 'a concurrent edit landed first', response.json['body'],
'the losing, stale update must never have been applied'
end
def test_real_update_with_a_fresh_lock_version_succeeds
post = PreviewFixtures::PreviewPost.create!(title: 'Racing edit', body: 'body')
patch("/posts/#{post.id}", params: { post: { title: 'Racing edit', body: 'my edit', lock_version: post.lock_version } })
assert_response :success
assert_equal 1, response.json['lock_version']
end
end