Files
fizzy/test/models/storage/no_reuse_test.rb
T
Jeremy Daer bd6c1cf34f Storage: harden reconcile for concurrent writes and fix board transfer (#2096)
* Storage: harden reconcile for concurrent writes and fix board transfer

Reconcile now uses two-cursor approach: captures cursor before and after
the storage scan, aborting if they differ (entries added during scan).
Job retries 3x with 1-minute waits and limits concurrency to 1 per owner.

Board transfer now correctly moves storage for card description embeds
and comment embeds, not just direct attachments. Uses batched queries
to handle cards with thousands of comments efficiently.

Also fixes N+1 queries in attachment grouping via lookup maps.

* Storage: fix per-attachment reconcile and enforce no blob reuse

The storage ledger tracks per-attachment (not per-blob) as a business
abstraction for quotas. This fixes reconcile to match that model and
adds enforcement to prevent blob reuse in tracked contexts.

* Reconcile now uses joins(:blob).sum() for per-attachment counting
* New validation prevents reusing blobs across tracked attachments
* Race-safe storage_total creation with create_or_find_by
* Job efficiency: skip find_by when object already available
* Backfill script checks per-attachment, not just per-blob
2025-12-19 13:07:32 -08:00

89 lines
2.8 KiB
Ruby

require "test_helper"
class Storage::NoReuseTest < ActiveSupport::TestCase
setup do
Current.session = sessions(:david)
@account = accounts("37s")
Current.account = @account # Ensure blobs get correct account_id
@board = @account.boards.create!(name: "Test", creator: users(:david))
end
# No-reuse validation
# NOTE: For persisted records, ActiveStorage::Attached::One#attach raises
# ActiveRecord::RecordNotSaved when validation fails.
test "rejects attaching blob that already has tracked attachment" do
blob = ActiveStorage::Blob.create_and_upload! \
io: StringIO.new("x" * 1000),
filename: "test.png",
content_type: "image/png"
# First attachment succeeds
card1 = @board.cards.create!(title: "Card 1", creator: users(:david))
card1.image.attach(blob)
assert card1.image.attached?
# Second attachment of same blob fails
card2 = @board.cards.create!(title: "Card 2", creator: users(:david))
assert_raises ActiveRecord::RecordNotSaved do
card2.image.attach(blob)
end
# Verify only one attachment exists for this blob
assert_equal 1, ActiveStorage::Attachment.where(blob_id: blob.id).count
end
test "rejects cross-account blob attachment" do
other_account = Account.create!(name: "Other")
other_board = other_account.boards.create!(name: "Other Board", creator: users(:david))
# Blob created in @account context
blob = ActiveStorage::Blob.create_and_upload! \
io: StringIO.new("x" * 1000),
filename: "test.png",
content_type: "image/png"
card = other_board.cards.create!(title: "Card", creator: users(:david))
assert_raises ActiveRecord::RecordNotSaved do
card.image.attach(blob)
end
# Verify attachment was not created (blob account doesn't match record account)
assert_not card.reload.image.attached?
end
test "allows attaching blob to untracked record type" do
file = file_fixture("moon.jpg")
blob = ActiveStorage::Blob.create_and_upload! \
io: file.open,
filename: "avatar.jpg",
content_type: "image/jpeg"
# User avatar is not a tracked record type
user = users(:david)
user.avatar.attach(blob)
# Should succeed - avatars are not storage-tracked
assert user.avatar.attached?
end
test "allows multiple attachments of same blob to untracked record types" do
file = file_fixture("moon.jpg")
blob = ActiveStorage::Blob.create_and_upload! \
io: file.open,
filename: "avatar.jpg",
content_type: "image/jpeg"
# First attachment to untracked (avatar)
user1 = users(:david)
user1.avatar.attach(blob)
assert user1.avatar.attached?
# Second attachment to untracked (another avatar) should work
# since no-reuse only checks tracked contexts
user2 = users(:jz)
user2.avatar.attach(blob)
assert user2.avatar.attached?
end
end