Deterministic fixture UUIDs

This commit is contained in:
Kevin McConnell
2025-11-12 12:28:29 +00:00
committed by Mike Dalessio
parent 1959e15f7e
commit 63f8a9af78
5 changed files with 58 additions and 5 deletions
+1 -1
View File
@@ -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
+52
View File
@@ -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')
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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