Deterministic fixture UUIDs
This commit is contained in:
committed by
Mike Dalessio
parent
1959e15f7e
commit
63f8a9af78
@@ -2,6 +2,6 @@ module Account::Seedeable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def setup_customer_template
|
||||
Account::Seeder.new(self, users.active.first).seed
|
||||
Account::Seeder.new(self, users.active.order(created_at: :asc).first).seed
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,6 +10,58 @@ module UuidPrimaryKey
|
||||
uuid_to_base36(SecureRandom.uuid_v7)
|
||||
end
|
||||
|
||||
def self.generate_fixture_uuid(label)
|
||||
# Generate deterministic UUIDv7 for fixtures that sorts by fixture ID
|
||||
# This allows .first/.last to work as expected in tests
|
||||
fixture_int = Digest::MD5.hexdigest("fixtures#{label}").hex % (2**31)
|
||||
|
||||
# Use fixture_int as second offset from a fixed base time (1 year before 2025-01-01)
|
||||
# This ensures all fixtures are in the past, and new test records are newest
|
||||
base_time = Time.utc(2024, 1, 1, 0, 0, 0)
|
||||
timestamp = base_time + fixture_int.seconds
|
||||
|
||||
uuid_v7_with_timestamp(timestamp, label)
|
||||
end
|
||||
|
||||
def self.uuid_v7_with_timestamp(time, seed_string)
|
||||
# Generate UUIDv7 with custom timestamp and deterministic random bits
|
||||
# Format: 48-bit timestamp_ms | 12-bit random | 4-bit version | 62-bit random
|
||||
|
||||
timestamp_ms = (time.to_f * 1000).to_i
|
||||
|
||||
# 48-bit timestamp (milliseconds since epoch)
|
||||
bytes = []
|
||||
bytes[0] = (timestamp_ms >> 40) & 0xff
|
||||
bytes[1] = (timestamp_ms >> 32) & 0xff
|
||||
bytes[2] = (timestamp_ms >> 24) & 0xff
|
||||
bytes[3] = (timestamp_ms >> 16) & 0xff
|
||||
bytes[4] = (timestamp_ms >> 8) & 0xff
|
||||
bytes[5] = timestamp_ms & 0xff
|
||||
|
||||
# Derive deterministic "random" bits from seed_string
|
||||
hash = Digest::MD5.hexdigest(seed_string)
|
||||
|
||||
# 12-bit random + 4-bit version (0111 for v7)
|
||||
rand_a = hash[0...3].to_i(16) & 0xfff
|
||||
bytes[6] = ((rand_a >> 8) & 0x0f) | 0x70 # version 7
|
||||
bytes[7] = rand_a & 0xff
|
||||
|
||||
# 2-bit variant (10) + 62-bit random
|
||||
rand_b = hash[3...19].to_i(16) & ((2**62) - 1)
|
||||
bytes[8] = ((rand_b >> 56) & 0x3f) | 0x80 # variant 10
|
||||
bytes[9] = (rand_b >> 48) & 0xff
|
||||
bytes[10] = (rand_b >> 40) & 0xff
|
||||
bytes[11] = (rand_b >> 32) & 0xff
|
||||
bytes[12] = (rand_b >> 24) & 0xff
|
||||
bytes[13] = (rand_b >> 16) & 0xff
|
||||
bytes[14] = (rand_b >> 8) & 0xff
|
||||
bytes[15] = rand_b & 0xff
|
||||
|
||||
# Format as UUID string
|
||||
uuid = "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x" % bytes
|
||||
uuid_to_base36(uuid)
|
||||
end
|
||||
|
||||
def self.uuid_to_base36(uuid)
|
||||
# Convert standard UUID format to base36 (lowercase), padded to 25 chars
|
||||
uuid.delete('-').to_i(16).to_s(36).rjust(25, '0')
|
||||
|
||||
@@ -5,7 +5,7 @@ class Board::AccessibleTest < ActiveSupport::TestCase
|
||||
boards(:writebook).update! all_access: false
|
||||
|
||||
boards(:writebook).accesses.revise granted: users(:david, :jz), revoked: users(:kevin)
|
||||
assert_equal users(:david, :jz), boards(:writebook).users
|
||||
assert_equal users(:david, :jz).to_set, boards(:writebook).users.to_set
|
||||
|
||||
boards(:writebook).accesses.grant_to users(:kevin)
|
||||
assert_includes boards(:writebook).users.reload, users(:kevin)
|
||||
|
||||
@@ -39,7 +39,7 @@ class Notifier::EventNotifierTest < ActiveSupport::TestCase
|
||||
|
||||
Notifier.for(events(:logo_published)).notify
|
||||
|
||||
assert_equal cards(:logo), Notification.last.source.eventable
|
||||
assert_equal cards(:logo), Notification.order(created_at: :desc).first.source.eventable
|
||||
end
|
||||
|
||||
test "assignment events only create a notification for the assignee" do
|
||||
|
||||
+3
-2
@@ -80,8 +80,9 @@ module FixturesTestHelper
|
||||
label = label.to_s.delete_suffix("_uuid")
|
||||
end
|
||||
|
||||
return super(label, column_type) unless column_type == :uuid
|
||||
UuidPrimaryKey.uuid_to_base36(super(label, column_type))
|
||||
# Rails passes :string for varchar columns, so handle both :uuid and :string
|
||||
return super(label, column_type) unless column_type.in?([:uuid, :string])
|
||||
UuidPrimaryKey.generate_fixture_uuid(label)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user