Files
fizzy/test/models/storage/entry_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

176 lines
4.4 KiB
Ruby

require "test_helper"
class Storage::EntryTest < ActiveSupport::TestCase
setup do
@account = accounts("37s")
@board = boards(:writebook)
@card = cards(:logo)
end
test "record creates entry with positive delta" do
assert_difference "Storage::Entry.count", +1 do
entry = Storage::Entry.record \
account: @account,
board: @board,
recordable: @card,
delta: 1024,
operation: "attach"
assert_equal @account.id, entry.account_id
assert_equal @board.id, entry.board_id
assert_equal @card.class.name, entry.recordable_type
assert_equal @card.id, entry.recordable_id
assert_equal 1024, entry.delta
assert_equal "attach", entry.operation
end
end
test "record creates entry with negative delta" do
entry = Storage::Entry.record \
account: @account,
board: @board,
recordable: @card,
delta: -512,
operation: "detach"
assert_equal -512, entry.delta
assert_equal "detach", entry.operation
end
test "record returns nil and creates no entry when delta is zero" do
assert_no_difference "Storage::Entry.count" do
result = Storage::Entry.record \
account: @account,
board: @board,
recordable: @card,
delta: 0,
operation: "attach"
assert_nil result
end
end
test "record works with destroyed records (destroyed? check)" do
# Simulate a destroyed record - .id still works after destroy
@card.destroy
entry = Storage::Entry.record \
account: @account,
board: @board,
recordable: @card,
delta: 2048,
operation: "detach"
assert_equal @account.id, entry.account_id
assert_equal @board.id, entry.board_id
assert_equal "Card", entry.recordable_type
assert_equal @card.id, entry.recordable_id
end
test "record creates entry without board" do
entry = Storage::Entry.record \
account: @account,
board: nil,
recordable: @card,
delta: 1024,
operation: "attach"
assert_nil entry.board_id
end
test "record creates entry without recordable" do
entry = Storage::Entry.record \
account: @account,
board: @board,
recordable: nil,
delta: 1024,
operation: "reconcile"
assert_nil entry.recordable_type
assert_nil entry.recordable_id
end
test "record enqueues MaterializeJob for account" do
assert_enqueued_with job: Storage::MaterializeJob, args: [ @account ] do
Storage::Entry.record \
account: @account,
board: nil,
recordable: nil,
delta: 1024,
operation: "attach"
end
end
test "record enqueues MaterializeJob for board when board_id present" do
assert_enqueued_with job: Storage::MaterializeJob, args: [ @board ] do
Storage::Entry.record \
account: @account,
board: @board,
recordable: nil,
delta: 1024,
operation: "attach"
end
end
test "record skips entirely when account is destroyed" do
# No need to track storage for deleted accounts
@account.destroy
assert_no_difference "Storage::Entry.count" do
result = Storage::Entry.record \
account: @account,
delta: 1024,
operation: "attach"
assert_nil result
end
end
test "record does not enqueue board job when board is destroyed" do
@board.destroy
# Account job still enqueued, but destroyed board skips its job
jobs = []
assert_enqueued_with job: Storage::MaterializeJob, args: [ @account ] do
Storage::Entry.record \
account: @account,
board: @board,
delta: 1024,
operation: "attach"
end
# Verify board job was NOT enqueued
board_jobs = enqueued_jobs.select { |j| j["arguments"].include?(@board.to_global_id.to_s) }
assert_empty board_jobs
end
test "entries belong to account" do
entry = Storage::Entry.record \
account: @account,
delta: 1024,
operation: "attach"
assert_equal @account, entry.account
end
test "entries belong to board (optional)" do
entry = Storage::Entry.record \
account: @account,
board: @board,
delta: 1024,
operation: "attach"
assert_equal @board, entry.board
end
test "entries belong to recordable (polymorphic, optional)" do
entry = Storage::Entry.record \
account: @account,
recordable: @card,
delta: 1024,
operation: "attach"
assert_equal @card, entry.recordable
end
end