feat(testing): add Prism and Testing

This commit is contained in:
dualfroz committed 2026-09-08 21:51:06 +02:00
1 parent bf0042120f
commit 0b6cf1b91e
1 file changed
+144
+144
View File
@@ -0,0 +1,144 @@
require 'uri'
require 'stringio'
module Prism
module Testing
# A single simulated "browser" talking to a Rack-style app in-process:
# no socket, no server, just direct #call. Keeps a cookie jar across
# requests within the session (the way Prism::Response#set_cookie /
# Request#parse_cookies expect: raw CGI-escaped values passed through
# unchanged) so multi-request flows (login, then an authenticated
# request; CSRF token issued, then submitted) can be exercised for real
# instead of stitched together by hand in every test file.
class IntegrationSession
attr_reader :response
def initialize(app)
@app = app
@cookies = {}
@response = nil
end
def get(path, params: nil, headers: {}, remote_addr: '127.0.0.1')
process('GET', path, params: params, headers: headers, remote_addr: remote_addr)
end
def head(path, params: nil, headers: {}, remote_addr: '127.0.0.1')
process('HEAD', path, params: params, headers: headers, remote_addr: remote_addr)
end
def delete(path, params: nil, headers: {}, remote_addr: '127.0.0.1')
process('DELETE', path, params: params, headers: headers, remote_addr: remote_addr)
end
def post(path, params: nil, headers: {}, remote_addr: '127.0.0.1')
process('POST', path, params: params, headers: headers, remote_addr: remote_addr)
end
def patch(path, params: nil, headers: {}, remote_addr: '127.0.0.1')
process('PATCH', path, params: params, headers: headers, remote_addr: remote_addr)
end
def put(path, params: nil, headers: {}, remote_addr: '127.0.0.1')
process('PUT', path, params: params, headers: headers, remote_addr: remote_addr)
end
def follow_redirect!
location = response && response.location
raise 'no redirect to follow (last response was not a 3xx with a Location header)' unless location
uri = URI.parse(location)
target = uri.path.to_s
target = "#{target}?#{uri.query}" if uri.query
get(target)
end
def cookies
@cookies.dup
end
def clear!
@cookies = {}
@response = nil
end
private
QUERY_METHODS = %w[GET HEAD DELETE].freeze
def process(method, path, params:, headers:, remote_addr:)
path_part, existing_query = path.to_s.split('?', 2)
content_type = nil
body = ''
query = existing_query
if params
if QUERY_METHODS.include?(method)
encoded = encode_params(params)
query = [existing_query, encoded].compact.reject(&:empty?).join('&')
elsif params.is_a?(String)
body = params
else
body = encode_params(params)
content_type = 'application/x-www-form-urlencoded'
end
end
env = build_env(method, path_part, query, body, headers, content_type, remote_addr)
status, response_headers, body_parts = @app.call(env)
@response = TestResponse.new(status, response_headers, body_parts)
absorb_cookies(@response)
@response
end
def build_env(method, path_part, query, body, headers, content_type, remote_addr)
env = {
'REQUEST_METHOD' => method,
'PATH_INFO' => path_part,
'QUERY_STRING' => query.to_s,
'rack.input' => StringIO.new(body || ''),
'CONTENT_LENGTH' => (body ? body.bytesize.to_s : '0'),
'REMOTE_ADDR' => remote_addr,
'SERVER_NAME' => 'test.host',
'rack.url_scheme' => 'http'
}
env['CONTENT_TYPE'] = content_type if content_type
headers.each do |name, value|
key = name.to_s
case key.downcase
when 'content-type' then env['CONTENT_TYPE'] = value
when 'content-length' then env['CONTENT_LENGTH'] = value.to_s
else env["HTTP_#{key.upcase.tr('-', '_')}"] = value
end
end
cookie_header = cookie_header_value
env['HTTP_COOKIE'] = cookie_header if cookie_header
env
end
def cookie_header_value
return nil if @cookies.empty?
@cookies.map { |name, value| "#{name}=#{value}" }.join('; ')
end
def absorb_cookies(response)
response.set_cookies.each { |name, value| @cookies[name] = value }
end
def encode_params(params, prefix = nil)
case params
when Hash
params.map { |key, value| encode_params(value, prefix ? "#{prefix}[#{key}]" : key.to_s) }
.join('&')
when Array
params.map { |value| encode_params(value, "#{prefix}[]") }.join('&')
else
"#{URI.encode_www_form_component(prefix.to_s)}=#{URI.encode_www_form_component(params.to_s)}"
end
end
end
end
end