mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 08:32:15 +00:00
Three findings from the automated review passes, all confirmed against the code before changing anything. The download URL was dead on arrival for the caller it was built for. Sure serves stored files through Active Storage controllers that config/initializers/active_storage_authorization.rb gates on `viewable_by?(Current.user)` — a signed-in browser session. An MCP client has a bearer token and no session, so following the URL would have redirected to sign-in. Removed it rather than leaving a link that cannot work, and the description now points at search_family_files or the vault UI. Coverage called a month `covered` when a document merely existed. An unreconciled statement is not mismatched, so it took the `covered` branch, and the payload carried nothing to correct the reading — the same "advertised verification that never happened" bug fixed last round in get_account_statement, in a second place. Months now carry their own reconciliation_status, and the description says covered means presence, not agreement. Listing filtered visibility after limiting. Beyond underfilling a page, with no cursor and a 100-row cap an accessible statement behind enough newer invisible ones was unreachable. Visibility now lives in the query, mirroring viewable_by? for a statement manager. Also: rescue unexpected upload failures into a tool error instead of a raw exception string, derive the documented size limit from MAX_FILE_SIZE, list every coverage status in mcp.md, and cover the failed-reconciliation and base64-normalisation branches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JFDp9HhXDeswadu4cxFojn
150 lines
5.1 KiB
Ruby
150 lines
5.1 KiB
Ruby
require "test_helper"
|
|
|
|
class Assistant::Function::UploadAccountStatementTest < ActiveSupport::TestCase
|
|
setup do
|
|
@user = users(:family_admin)
|
|
@account = accounts(:depository)
|
|
@function = Assistant::Function::UploadAccountStatement.new(@user)
|
|
@content = "date,amount\n2024-01-01,1\n"
|
|
end
|
|
|
|
test "has correct name and is not strict" do
|
|
assert_equal "upload_account_statement", @function.name
|
|
assert_not @function.strict_mode?
|
|
assert_includes @function.params_schema[:required], "content_base64"
|
|
end
|
|
|
|
test "stores a statement in the vault" do
|
|
result = nil
|
|
|
|
assert_difference "AccountStatement.count", 1 do
|
|
result = @function.call(params(filename: "statement.csv"))
|
|
end
|
|
|
|
assert result[:success]
|
|
assert_not result[:duplicate]
|
|
assert_equal Digest::SHA256.hexdigest(@content), result[:statement][:content_sha256]
|
|
assert_equal "statement.csv", result[:statement][:filename]
|
|
end
|
|
|
|
test "re-uploading identical bytes returns the existing statement without creating a row" do
|
|
first = @function.call(params(filename: "statement.csv"))
|
|
|
|
assert_no_difference "AccountStatement.count" do
|
|
second = @function.call(params(filename: "different-name.csv"))
|
|
|
|
assert second[:success]
|
|
assert second[:duplicate]
|
|
assert_equal first[:statement][:id], second[:statement][:id]
|
|
end
|
|
end
|
|
|
|
test "links to an account when one is given" do
|
|
result = @function.call(params(filename: "statement.csv", account_id: @account.id))
|
|
|
|
assert result[:success]
|
|
assert_equal @account.id, result[:statement][:account][:id]
|
|
assert_equal "linked", result[:statement][:review_status]
|
|
end
|
|
|
|
test "leaves the statement unmatched when no account is given" do
|
|
result = @function.call(params(filename: "statement.csv"))
|
|
|
|
assert_equal "unmatched", result[:statement][:review_status]
|
|
assert_nil result[:statement][:account]
|
|
end
|
|
|
|
test "reports a duplicate without disclosing a statement filed against a hidden account" do
|
|
@function.call(params(filename: "statement.csv", account_id: accounts(:other_asset).id))
|
|
|
|
result = Assistant::Function::UploadAccountStatement.new(users(:family_member))
|
|
.call(params(filename: "statement.csv"))
|
|
|
|
assert result[:success]
|
|
assert result[:duplicate]
|
|
assert_equal Digest::SHA256.hexdigest(@content), result[:statement][:content_sha256]
|
|
assert_nil result[:statement][:account]
|
|
assert_nil result[:statement][:filename]
|
|
end
|
|
|
|
test "refuses a user who cannot manage the vault" do
|
|
result = Assistant::Function::UploadAccountStatement.new(family_guest).call(params(filename: "statement.csv"))
|
|
|
|
assert_not result[:success]
|
|
assert_equal "forbidden", result[:error]
|
|
end
|
|
|
|
test "rejects an unsupported file type" do
|
|
result = @function.call(params(filename: "notes.txt"))
|
|
|
|
assert_not result[:success]
|
|
assert_equal "unsupported_file_type", result[:error]
|
|
end
|
|
|
|
test "rejects content that is not base64" do
|
|
result = @function.call("filename" => "statement.csv", "content_base64" => "not base64 @@@")
|
|
|
|
assert_not result[:success]
|
|
assert_equal "invalid_content", result[:error]
|
|
end
|
|
|
|
test "accepts base64 wrapped across lines" do
|
|
wrapped = Base64.strict_encode64(@content).scan(/.{1,8}/).join("\n")
|
|
|
|
result = @function.call("filename" => "statement.csv", "content_base64" => wrapped)
|
|
|
|
assert result[:success]
|
|
assert_equal Digest::SHA256.hexdigest(@content), result[:statement][:content_sha256]
|
|
end
|
|
|
|
test "accepts urlsafe base64 without padding" do
|
|
encoded = Base64.urlsafe_encode64(@content, padding: false)
|
|
|
|
result = @function.call("filename" => "statement.csv", "content_base64" => encoded)
|
|
|
|
assert result[:success]
|
|
assert_equal Digest::SHA256.hexdigest(@content), result[:statement][:content_sha256]
|
|
end
|
|
|
|
test "rejects content that decodes to zero bytes" do
|
|
result = @function.call("filename" => "statement.csv", "content_base64" => Base64.strict_encode64(""))
|
|
|
|
assert_not result[:success]
|
|
assert_equal "invalid_content", result[:error]
|
|
end
|
|
|
|
test "reports an unexpected storage failure as a tool error" do
|
|
AccountStatement.stubs(:create_from_prepared_upload!).raises(StandardError, "storage exploded")
|
|
|
|
result = @function.call(params(filename: "statement.csv"))
|
|
|
|
assert_not result[:success]
|
|
assert_equal "upload_failed", result[:error]
|
|
assert_match(/storage exploded/, result[:message])
|
|
end
|
|
|
|
test "rejects an unknown account_id rather than silently uploading unlinked" do
|
|
result = @function.call(params(filename: "statement.csv", account_id: SecureRandom.uuid))
|
|
|
|
assert_not result[:success]
|
|
assert_equal "account_not_found", result[:error]
|
|
end
|
|
|
|
test "rejects a file whose contents do not match its extension" do
|
|
result = @function.call(
|
|
"filename" => "statement.pdf",
|
|
"content_base64" => Base64.strict_encode64("this is not a pdf")
|
|
)
|
|
|
|
assert_not result[:success]
|
|
assert_equal "invalid_file", result[:error]
|
|
end
|
|
|
|
private
|
|
def params(filename:, account_id: nil, content: @content)
|
|
{ "filename" => filename, "content_base64" => Base64.strict_encode64(content) }.tap do |p|
|
|
p["account_id"] = account_id if account_id
|
|
end
|
|
end
|
|
end
|