test: cover PreviewFixtures and PreviewPost
This commit is contained in:
1 parent
3764cd9822
commit
726e1cd908
1 file changed
+128
@@ -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
|
||||||
Reference in new issue
Block a user