From 730c66caf0917abe4d798434ac43d80fd2354c4e Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 17:30:12 +0200 Subject: [PATCH] test: cover AuthzArticlePolicy, destroy? and CacheHelpersTest --- test/test_authorizable.rb | 113 +++++++++++++++++++++++++++++++++++++ test/test_cache_helpers.rb | 67 ++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 test/test_authorizable.rb create mode 100644 test/test_cache_helpers.rb diff --git a/test/test_authorizable.rb b/test/test_authorizable.rb new file mode 100644 index 0000000..2ebdf3d --- /dev/null +++ b/test/test_authorizable.rb @@ -0,0 +1,113 @@ +require 'minitest/autorun' +require 'prism' +require 'support' +require 'tmpdir' + +class AuthzArticlePolicy < Prism::Policy + def destroy? + user && record.user_id == user.id + end +end + +class AuthzSessionsController < Prism::Controller + include Prism::Authenticatable + authenticate_with model: :AuthzUser + + def create + user = AuthzUser.find_by(email: params['email']) + if user + sign_in(user) + halt(200, 'signed in') + else + halt(401, 'invalid credentials') + end + end +end + +class ArticlesController < Prism::Controller + include Prism::Authenticatable + include Prism::Authorizable + authenticate_with model: :AuthzUser + before_action :require_authentication, only: %i[destroy] + rescue_from Prism::Policy::NotAuthorizedError, with: :forbidden + + def destroy + article = AuthzArticle.find(params['id']) + authorize(article) + article.destroy + halt(200, 'deleted') + rescue Prism::Orm::RecordNotFound + halt(404, 'not found') + end + + private + + def forbidden(_exception) + halt(403, 'forbidden') + end +end + +class AuthorizableApp < Prism::Application + enable_sessions(secret: 'authz-secret') + post '/sign_in', to: 'authz_sessions#create' + delete '/articles/:id', to: 'articles#destroy' +end + +class AuthorizableTest < Minitest::Test + include TestSupport + + def setup + @dir = Dir.mktmpdir('prism-authorizable') + @store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore')) + @store.create_table(:authz_users, %w[id email]) + @store.create_table(:authz_articles, %w[id title user_id]) + + user_class = Class.new(Prism::Orm::Model) { table_name :authz_users } + user_class.store = @store + Object.const_set(:AuthzUser, user_class) + + article_class = Class.new(Prism::Orm::Model) { table_name :authz_articles } + article_class.store = @store + Object.const_set(:AuthzArticle, article_class) + + @owner = AuthzUser.create!(email: 'owner@example.com') + @stranger = AuthzUser.create!(email: 'stranger@example.com') + @article = AuthzArticle.create!(title: 'mine', user_id: @owner.id) + end + + def teardown + FileUtils.remove_entry(@dir) + Object.send(:remove_const, :AuthzUser) if Object.const_defined?(:AuthzUser) + Object.send(:remove_const, :AuthzArticle) if Object.const_defined?(:AuthzArticle) + end + + def call(method, path, **opts) + AuthorizableApp.call(env_for(method, path, **opts)) + end + + def cookie_for(email) + _, headers, = call('POST', '/sign_in', + headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }, + body: URI.encode_www_form('email' => email)) + lines = headers['Set-Cookie'].split("\n") + lines.find { |line| line.start_with?('prism.session=') }.split(';').first + end + + def test_owner_may_destroy_their_own_article + status, _, body = call('DELETE', "/articles/#{@article.id}", cookies: cookie_for('owner@example.com')) + assert_equal 200, status + assert_equal 'deleted', body.join + end + + def test_stranger_gets_403_instead_of_destroying_someone_elses_article + status, _, body = call('DELETE', "/articles/#{@article.id}", cookies: cookie_for('stranger@example.com')) + assert_equal 403, status + assert_equal 'forbidden', body.join + assert AuthzArticle.find_by(id: @article.id), 'the article must survive a denied authorization' + end + + def test_signed_out_request_never_reaches_the_policy_at_all + status, = call('DELETE', "/articles/#{@article.id}") + assert_equal 401, status + end +end diff --git a/test/test_cache_helpers.rb b/test/test_cache_helpers.rb new file mode 100644 index 0000000..437bf31 --- /dev/null +++ b/test/test_cache_helpers.rb @@ -0,0 +1,67 @@ +require 'minitest/autorun' +require 'prism' +require 'tmpdir' +require 'fileutils' + +class CacheHelpersTest < Minitest::Test + def setup + Prism::Cache.store.clear + @dir = Dir.mktmpdir('prism-cache-helpers') + FileUtils.mkdir_p(File.join(@dir, 'pages')) + @view = Prism::View.new(@dir) + end + + def teardown + FileUtils.remove_entry(@dir) + Prism::Cache.store.clear + end + + def write(relative, source) + File.write(File.join(@dir, relative), source) + end + + def test_cache_block_renders_and_stores_the_fragment + write('pages/show.erb', '<% cache("greeting") do %><%= greeting %><% end %>') + result = @view.render('pages/show', locals: { greeting: 'hi' }) + assert_equal 'hi', result + assert_equal 'hi', Prism::Cache.store.read('greeting') + end + + def test_cache_block_is_not_reevaluated_on_a_hit + calls = [] + write('pages/show.erb', '<% cache("counter") do %><%= track! %><% end %>') + @view = Prism::View.new(@dir, helpers: CounterHelper.new(calls)) + + first = @view.render('pages/show') + second = @view.render('pages/show') + + assert_equal first, second + assert_equal 1, calls.size + end + + def test_cache_key_can_be_an_array_mixing_scope_and_a_model + model = Struct.new(:cache_key).new('posts/7-42') + write('pages/show.erb', '<% cache([:sidebar, model]) do %>rendered<% end %>') + @view.render('pages/show', locals: { model: model }) + assert @view.expand_cache_key([:sidebar, model]) == 'sidebar/posts/7-42' + assert_equal 'rendered', Prism::Cache.store.read('sidebar/posts/7-42') + end + + def test_expires_in_evicts_the_entry_after_the_given_time + write('pages/show.erb', '<% cache("temp", expires_in: 0) do %>value<% end %>') + @view.render('pages/show') + sleep 0.01 + assert_nil Prism::Cache.store.read('temp') + end + + class CounterHelper + def initialize(calls) + @calls = calls + end + + def track! + @calls << true + 'x' + end + end +end