Files
ValidBoard/test/test_helper.rb
T
Tom FluxandClaude Opus 5 d94984cd97 Add tests for the store and the API
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
2026-08-14 22:41:11 +01:00

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