Rack::MockRequest with a cookie jar rather than rack-test, so the suite needs nothing beyond the Debian packages the app already uses. Covers key validation and batch atomicity, password hashing, the schema version guard, auth and lockout, the storage API, and that a note containing a closing script tag can't break out of the bootstrap block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UU1vyTHj3uE9PJYSxRxwkU
99 lines
2.7 KiB
Ruby
99 lines
2.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'minitest/autorun'
|
|
require 'tmpdir'
|
|
require 'fileutils'
|
|
require 'json'
|
|
require 'rack'
|
|
require 'rack/mock'
|
|
|
|
# Point the app at a throwaway database *before* loading it — app.rb opens the
|
|
# store at require time.
|
|
TEST_DIR = Dir.mktmpdir('validboard-test')
|
|
|
|
ENV['VALIDBOARD_DB'] = File.join(TEST_DIR, 'test.db')
|
|
ENV.delete('VALIDBOARD_SECRET')
|
|
ENV.delete('VALIDBOARD_SECURE_COOKIE')
|
|
|
|
Minitest.after_run { FileUtils.remove_entry(TEST_DIR, true) }
|
|
|
|
require_relative '../app'
|
|
|
|
TEST_PASSWORD = 'correct-horse-battery'
|
|
|
|
# A cookie-carrying client. Rack::MockRequest doesn't keep a jar of its own, so
|
|
# without this every request would look like a fresh browser and nothing that
|
|
# depends on a session could be tested.
|
|
class Client
|
|
COOKIE_NAME = 'validboard.session'
|
|
|
|
def initialize(remote_addr: '127.0.0.1')
|
|
@mock = Rack::MockRequest.new(ValidBoard::App)
|
|
@cookies = {}
|
|
@remote_addr = remote_addr
|
|
end
|
|
|
|
def get(path, env = {}) = request('GET', path, env)
|
|
def post(path, env = {}) = request('POST', path, env)
|
|
def delete(path, env = {}) = request('DELETE', path, env)
|
|
|
|
def post_json(path, obj, env = {})
|
|
post(path, env.merge(input: JSON.generate(obj), 'CONTENT_TYPE' => 'application/json'))
|
|
end
|
|
|
|
def login(password = TEST_PASSWORD)
|
|
post('/login', params: { 'password' => password })
|
|
end
|
|
|
|
def session_cookie
|
|
@cookies[COOKIE_NAME]
|
|
end
|
|
|
|
private
|
|
|
|
def request(method, path, env)
|
|
# Rack::MockRequest leaves HTTP_VERSION unset, which reads as HTTP/1.0 —
|
|
# and Sinatra answers a POST redirect with 302 there but 303 over 1.1. Pin
|
|
# it so the tests see what a browser will.
|
|
env = { 'REMOTE_ADDR' => @remote_addr, 'HTTP_VERSION' => 'HTTP/1.1' }.merge(env)
|
|
env['HTTP_COOKIE'] = @cookies.map { |k, v| "#{k}=#{v}" }.join('; ') unless @cookies.empty?
|
|
|
|
response = @mock.request(method, path, env)
|
|
absorb_cookies(response)
|
|
response
|
|
end
|
|
|
|
def absorb_cookies(response)
|
|
raw = response.headers['Set-Cookie']
|
|
return if raw.nil?
|
|
|
|
Array(raw).flat_map { |header| header.split("\n") }.each do |line|
|
|
name, value = line.split(';', 2).first.to_s.split('=', 2)
|
|
next if name.nil?
|
|
|
|
@cookies[name] = value.to_s
|
|
end
|
|
end
|
|
end
|
|
|
|
module TestHelpers
|
|
# Puts the app's store back to a known state: one password, no boards.
|
|
def reset_app_store!
|
|
ValidBoard::STORE.wipe_items!
|
|
ValidBoard::STORE.password = TEST_PASSWORD
|
|
end
|
|
|
|
def signed_in_client(**kwargs)
|
|
client = Client.new(**kwargs)
|
|
response = client.login
|
|
|
|
raise "login failed: #{response.status}" unless response.status == 303
|
|
|
|
client
|
|
end
|
|
|
|
def json_body(response)
|
|
JSON.parse(response.body)
|
|
end
|
|
end
|