From 4786db1de5d77ac21af4b0c4ca13ceda729ba805 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 10 Sep 2026 15:04:26 +0200 Subject: [PATCH] test: cover CorsArticlesController, show and CsrfTest --- test/test_cors_conditional_get.rb | 90 +++++++++++++++++++++++++++++++ test/test_csrf.rb | 51 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 test/test_cors_conditional_get.rb create mode 100644 test/test_csrf.rb diff --git a/test/test_cors_conditional_get.rb b/test/test_cors_conditional_get.rb new file mode 100644 index 0000000..0d10e6f --- /dev/null +++ b/test/test_cors_conditional_get.rb @@ -0,0 +1,90 @@ +require 'minitest/autorun' +require 'prism' +require 'support' + +# Session 52's documented debt: Cors and ConditionalGet were each tested in +# isolation, but never together - and the two subsystems interact in a way +# that is easy to get wrong. Prism::Middleware::Cors stamps headers onto +# whatever [status, headers, body] the app below it returns +# (lib/prism/middleware/cors.rb#call), without special-casing any status +# code, and Controller#halt(304, '') (used by fresh_when/stale?) short- +# circuits rendering but still returns through the SAME response path as a +# normal 200 - so a cross-origin conditional GET that turns out fresh +# should still come back as a CORS-tagged 304 with an empty body, not lose +# its Access-Control-* headers just because nothing was actually rendered. +class CorsArticlesController < Prism::Controller + ETAG = 'cors-article-v1'.freeze + + def show + fresh_when(etag: ETAG) + html('full article body') + end +end + +class CorsConditionalGetApp < Prism::Application + get '/api/articles/1', to: 'cors_articles#show' +end + +class CorsConditionalGetTest < Minitest::Test + include TestSupport + + ORIGIN = 'https://app.example.com'.freeze + FRESH_ETAG = 'W/"' + Digest::SHA1.hexdigest(CorsArticlesController::ETAG) + '"' + + def setup + Prism::Cors.reset! + Prism::Cors.configure do |config| + config.allow do + origins ORIGIN + resource '/api/*', methods: %w[GET] + end + end + @app = Prism::Middleware::Cors.new(CorsConditionalGetApp) + end + + def teardown + Prism::Cors.reset! + end + + def call(headers) + @app.call(env_for('GET', '/api/articles/1', headers: headers)) + end + + def test_first_cross_origin_request_renders_and_still_gets_cors_headers + status, headers, body = call('Origin' => ORIGIN) + + assert_equal 200, status + assert_equal 'full article body', body.join + assert_equal FRESH_ETAG, headers['ETag'] + assert_equal ORIGIN, headers['Access-Control-Allow-Origin'] + assert_equal 'Origin', headers['Vary'] + end + + def test_fresh_cross_origin_request_gets_304_with_cors_headers_intact + status, headers, body = call('Origin' => ORIGIN, 'If-None-Match' => FRESH_ETAG) + + assert_equal 304, status + assert_equal '', body.join + assert_equal FRESH_ETAG, headers['ETag'], 'ETag must still be set on a 304, the client needs it to keep caching' + assert_equal ORIGIN, headers['Access-Control-Allow-Origin'], + 'a 304 short-circuited by halt() must not lose the CORS headers Cors already knows to stamp on any status' + assert_equal 'Origin', headers['Vary'] + end + + def test_fresh_same_origin_request_gets_304_without_cors_headers + status, headers, body = call('If-None-Match' => FRESH_ETAG) + + assert_equal 304, status + assert_equal '', body.join + assert_equal FRESH_ETAG, headers['ETag'] + assert_nil headers['Access-Control-Allow-Origin'] + end + + def test_stale_cross_origin_request_from_a_disallowed_origin_still_renders_without_cors_headers + status, headers, body = call('Origin' => 'https://evil.example.com', 'If-None-Match' => FRESH_ETAG) + + assert_equal 304, status, 'ConditionalGet does not know or care about CORS - freshness alone decides the status' + assert_equal '', body.join + assert_nil headers['Access-Control-Allow-Origin'] + end +end diff --git a/test/test_csrf.rb b/test/test_csrf.rb new file mode 100644 index 0000000..7fb325e --- /dev/null +++ b/test/test_csrf.rb @@ -0,0 +1,51 @@ +require 'minitest/autorun' +require 'prism' + +class CsrfTest < Minitest::Test + def setup + @session = {} + @csrf = Prism::Csrf.new(@session) + end + + def test_raw_token_is_persisted_in_session + token = @csrf.raw_token + assert_equal token, @session[Prism::Csrf::SESSION_KEY] + end + + def test_raw_token_is_stable_across_calls + assert_equal @csrf.raw_token, @csrf.raw_token + end + + def test_masked_token_differs_each_call + refute_equal @csrf.masked_token, @csrf.masked_token + end + + def test_masked_token_is_valid + assert @csrf.valid_token?(@csrf.masked_token) + end + + def test_multiple_masked_tokens_from_same_raw_token_all_validate + tokens = Array.new(5) { @csrf.masked_token } + assert tokens.all? { |token| @csrf.valid_token?(token) } + assert_equal 5, tokens.uniq.size + end + + def test_token_from_different_session_is_rejected + other_session = {} + other_csrf = Prism::Csrf.new(other_session) + refute @csrf.valid_token?(other_csrf.masked_token) + end + + def test_blank_or_garbage_tokens_are_rejected + refute @csrf.valid_token?(nil) + refute @csrf.valid_token?('') + refute @csrf.valid_token?('not-base64!!!') + refute @csrf.valid_token?('dGhpcyBpcyB0b28gc2hvcnQ=') + end + + def test_tampering_with_masked_token_invalidates_it + masked = @csrf.masked_token + tampered = masked.sub(/.\z/) { |c| c == 'a' ? 'b' : 'a' } + refute @csrf.valid_token?(tampered) + end +end