diff --git a/Library/Static Classes/V7/V7 - Const.cs b/Library/Static Classes/V7/V7 - Const.cs index cb78ccc..fdda7e0 100644 --- a/Library/Static Classes/V7/V7 - Const.cs +++ b/Library/Static Classes/V7/V7 - Const.cs @@ -10,4 +10,10 @@ public static partial class V7 { private static readonly Vector128 _VersionMask = Format.VersionVariantMaskNot(V7.Version, V7.Variant); private static readonly Vector128 _VersionOverlay = Format.VersionVariantOverlayer(V7.Version, V7.Variant); + + /// Unix epoch: January 1, 1970 00:00:00 UTC + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// Maximum value that can fit in 48 bits (for RFC9562 V7 timestamp) + private const UInt64 Max48BitValue = 0xFFFFFFFFFFFF; } diff --git a/Library/Static Classes/V7/V7 - Extract.cs b/Library/Static Classes/V7/V7 - Extract.cs index 0947a08..a114faf 100644 --- a/Library/Static Classes/V7/V7 - Extract.cs +++ b/Library/Static Classes/V7/V7 - Extract.cs @@ -1,9 +1,9 @@ namespace DaanV2.UUID; public static partial class V7 { - /// Extracts the UTC from the UUID - /// The to extract the UTC from - /// The UTC value + /// Extracts the Unix milliseconds timestamp from the UUID + /// The to extract the timestamp from + /// Unix timestamp in milliseconds public static UInt64 ExtractUtc(UUID uuid) { (UInt64 bits48, _, _) = Format.Extract(uuid); @@ -11,11 +11,24 @@ public static UInt64 ExtractUtc(UUID uuid) { } /// Extracts the datetime from the UUID - /// The to extract the UTC from + /// The to extract the timestamp from /// The public static DateTime Extract(UUID uuid) { - UInt64 fileUTC = ExtractUtc(uuid); + UInt64 unixMs = ExtractUtc(uuid); - return DateTime.FromFileTimeUtc((Int64)fileUTC); + return UnixMillisecondsToDateTime(unixMs); + } + + /// Converts Unix milliseconds to DateTime + /// Milliseconds since Unix epoch (1970-01-01 00:00:00 UTC) + /// The corresponding DateTime in UTC + private static DateTime UnixMillisecondsToDateTime(UInt64 unixMs) { + try { + return UnixEpoch.AddMilliseconds(unixMs); + } + catch (ArgumentOutOfRangeException) { + // If the milliseconds value is too large, return MaxValue + return DateTime.MaxValue; + } } } diff --git a/Library/Static Classes/V7/V7 - Generate.cs b/Library/Static Classes/V7/V7 - Generate.cs index 1f537a9..5199ecc 100644 --- a/Library/Static Classes/V7/V7 - Generate.cs +++ b/Library/Static Classes/V7/V7 - Generate.cs @@ -65,15 +65,32 @@ public static UUID Generate(UInt64 utc, ReadOnlySpan bytes10) { /// public static UUID Generate(DateTime timestamp, UInt16 randomA, UInt64 randomB) { - UInt64 t = (UInt64)timestamp.ToFileTimeUtc(); + // RFC9562: V7 uses Unix epoch timestamp in milliseconds (48 bits) + UInt64 unixMs = DateTimeToUnixMilliseconds(timestamp); - return Generate(t, randomA, randomB); + return Generate(unixMs, randomA, randomB); } /// - public static UUID Generate(UInt64 utc, UInt16 randomA, UInt64 randomB) { - Vector128 u = Format.Create(V7.Version, V7.Variant, utc, randomA, randomB); + /// Unix timestamp in milliseconds (48-bit value) + /// 12 bits of random data + /// 62 bits of random data + public static UUID Generate(UInt64 unixMs, UInt16 randomA, UInt64 randomB) { + Vector128 u = Format.Create(V7.Version, V7.Variant, unixMs, randomA, randomB); return new UUID(u); } + + /// Converts a DateTime to Unix milliseconds + /// The DateTime to convert + /// Milliseconds since Unix epoch (1970-01-01 00:00:00 UTC) + private static UInt64 DateTimeToUnixMilliseconds(DateTime timestamp) { + var milliseconds = (timestamp.ToUniversalTime() - UnixEpoch).TotalMilliseconds; + + // Ensure non-negative and within 48-bit range + if (milliseconds < 0) milliseconds = 0; + if (milliseconds > Max48BitValue) milliseconds = Max48BitValue; + + return (UInt64)milliseconds; + } } diff --git a/Tests/Generation/V7Tests.cs b/Tests/Generation/V7Tests.cs index f862cf7..08b7e8a 100644 --- a/Tests/Generation/V7Tests.cs +++ b/Tests/Generation/V7Tests.cs @@ -5,7 +5,12 @@ public sealed partial class V7Tests { // From https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-example-of-a-uuidv7-value [Fact(DisplayName = "Given a known example, will generate the expected UUID")] public void TestVector() { - var timestamp = DateTime.FromFileTimeUtc(1645557742000); + // RFC9562 test vector: UUID 017f22e2-79b0-7cc3-98c4-dc0c0c07398f + // First 48 bits are Unix milliseconds: 0x017f22e279b0 = 1645557742000 + // This is 2022-02-22 19:22:22 UTC + var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var timestamp = unixEpoch.AddMilliseconds(1645557742000); + UInt16 randA = (UInt16)0xCC3; UInt64 randB = (UInt64)0x18C4DC0C0C07398F; @@ -13,8 +18,8 @@ public void TestVector() { Assert.Equal("017f22e2-79b0-7cc3-98c4-dc0c0c07398f", u.ToString()); - Int64 extractedUTC = (Int64)V7.ExtractUtc(u); - Assert.Equal(timestamp.ToFileTimeUtc(), extractedUTC); + UInt64 extractedMs = V7.ExtractUtc(u); + Assert.Equal(1645557742000UL, extractedMs); DateTime extracted = V7.Extract(u); Assert.Equal(timestamp, extracted); diff --git a/Tests/RFC/RFC4122.cs b/Tests/RFC/RFC4122.cs index d19bc03..92934f3 100644 --- a/Tests/RFC/RFC4122.cs +++ b/Tests/RFC/RFC4122.cs @@ -1,4 +1,5 @@ using DaanV2.UUID; +using System.Text; namespace Tests.RFC; @@ -12,4 +13,253 @@ public void RFC4122_AppendixB_V3_MD5_DNS_ExampleCom() { Utility.ValidateUUID(uuid, V3.Version, V3.Variant); } + // ========== Bit-Level Validation Tests ========== + + [Fact(DisplayName = "RFC4122 Section 4.1.3 - Version bits must be in correct position")] + public void RFC4122_VersionBits_CorrectPosition() { + // Version field is in octet 6, bits 12-15 of time_hi_and_version + var v1uuid = V1.Generate(); + var v3uuid = V3.Generate("test"); + var v4uuid = V4.Generate(); + var v5uuid = V5.Generate("test"); + + // Version should be readable from the UUID + Assert.Equal(DaanV2.UUID.Version.V1, v1uuid.Version); + Assert.Equal(DaanV2.UUID.Version.V3, v3uuid.Version); + Assert.Equal(DaanV2.UUID.Version.V4, v4uuid.Version); + Assert.Equal(DaanV2.UUID.Version.V5, v5uuid.Version); + } + + [Fact(DisplayName = "RFC4122 Section 4.1.1 - Variant bits must be 10x for RFC4122 UUIDs")] + public void RFC4122_VariantBits_RFC4122Compliant() { + // Variant field is in octet 8, bits 6-7 of clock_seq_hi_and_reserved + // Must be 10xxxxxx (0x80-0xBF) for RFC4122 compliant UUIDs + var v1uuid = V1.Generate(); + var v3uuid = V3.Generate("test"); + var v4uuid = V4.Generate(); + var v5uuid = V5.Generate("test"); + + // All should have variant V1 (RFC4122) + Assert.Equal(Variant.V1, v1uuid.Variant); + Assert.Equal(Variant.V1, v3uuid.Variant); + Assert.Equal(Variant.V1, v4uuid.Variant); + Assert.Equal(Variant.V1, v5uuid.Variant); + } + + // ========== Nil and Max UUID Tests (RFC4122 Section 4.1.7) ========== + + [Fact(DisplayName = "RFC4122 Section 4.1.7 - Nil UUID is all zeros")] + public void RFC4122_NilUUID_AllZeros() { + var nilUuid = UUID.Zero; + Assert.Equal("00000000-0000-0000-0000-000000000000", nilUuid.ToString().ToLower()); + } + + [Fact(DisplayName = "RFC4122 - Max UUID is all ones")] + public void RFC4122_MaxUUID_AllOnes() { + var maxUuid = UUID.Max; + Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", maxUuid.ToString().ToLower()); + } + + // ========== Determinism Tests for Name-Based UUIDs ========== + + [Fact(DisplayName = "RFC4122 Section 4.3 - V3 (MD5) is deterministic")] + public void RFC4122_V3_Deterministic() { + // Same input should always produce the same UUID + var input = "www.example.com"; + var uuid1 = V3.Generate(input); + var uuid2 = V3.Generate(input); + var uuid3 = V3.Generate(input); + + Assert.Equal(uuid1, uuid2); + Assert.Equal(uuid2, uuid3); + Assert.Equal(uuid1.ToString(), uuid2.ToString()); + } + + [Fact(DisplayName = "RFC4122 Section 4.3 - V5 (SHA1) is deterministic")] + public void RFC4122_V5_Deterministic() { + // Same input should always produce the same UUID + var input = "www.example.com"; + var uuid1 = V5.Generate(input); + var uuid2 = V5.Generate(input); + var uuid3 = V5.Generate(input); + + Assert.Equal(uuid1, uuid2); + Assert.Equal(uuid2, uuid3); + Assert.Equal(uuid1.ToString(), uuid2.ToString()); + } + + [Fact(DisplayName = "RFC4122 - V3 different inputs produce different UUIDs")] + public void RFC4122_V3_DifferentInputs_DifferentUUIDs() { + var uuid1 = V3.Generate("input1"); + var uuid2 = V3.Generate("input2"); + var uuid3 = V3.Generate("input3"); + + Assert.NotEqual(uuid1, uuid2); + Assert.NotEqual(uuid2, uuid3); + Assert.NotEqual(uuid1, uuid3); + } + + [Fact(DisplayName = "RFC4122 - V5 different inputs produce different UUIDs")] + public void RFC4122_V5_DifferentInputs_DifferentUUIDs() { + var uuid1 = V5.Generate("input1"); + var uuid2 = V5.Generate("input2"); + var uuid3 = V5.Generate("input3"); + + Assert.NotEqual(uuid1, uuid2); + Assert.NotEqual(uuid2, uuid3); + Assert.NotEqual(uuid1, uuid3); + } + + // ========== Round-Trip Tests ========== + + [Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V1")] + public void RFC4122_V1_RoundTrip() { + var original = V1.Generate(); + var str = original.ToString(); + var parsed = new UUID(str); + + Assert.Equal(original, parsed); + Assert.Equal(original.Version, parsed.Version); + Assert.Equal(original.Variant, parsed.Variant); + } + + [Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V3")] + public void RFC4122_V3_RoundTrip() { + var original = V3.Generate("test"); + var str = original.ToString(); + var parsed = new UUID(str); + + Assert.Equal(original, parsed); + Assert.Equal(original.Version, parsed.Version); + Assert.Equal(original.Variant, parsed.Variant); + } + + [Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V4")] + public void RFC4122_V4_RoundTrip() { + var original = V4.Generate(); + var str = original.ToString(); + var parsed = new UUID(str); + + Assert.Equal(original, parsed); + Assert.Equal(original.Version, parsed.Version); + Assert.Equal(original.Variant, parsed.Variant); + } + + [Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V5")] + public void RFC4122_V5_RoundTrip() { + var original = V5.Generate("test"); + var str = original.ToString(); + var parsed = new UUID(str); + + Assert.Equal(original, parsed); + Assert.Equal(original.Version, parsed.Version); + Assert.Equal(original.Variant, parsed.Variant); + } + + // ========== Format Tests ========== + + [Fact(DisplayName = "RFC4122 Section 3 - UUID format is 8-4-4-4-12 hexadecimal")] + public void RFC4122_Format_Correct() { + var uuid = V4.Generate(); + var str = uuid.ToString(); + + // Check format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + Assert.Equal(36, str.Length); + Assert.Equal('-', str[8]); + Assert.Equal('-', str[13]); + Assert.Equal('-', str[18]); + Assert.Equal('-', str[23]); + + // All other characters should be valid hex + var hexParts = str.Split('-'); + Assert.Equal(5, hexParts.Length); + Assert.Equal(8, hexParts[0].Length); + Assert.Equal(4, hexParts[1].Length); + Assert.Equal(4, hexParts[2].Length); + Assert.Equal(4, hexParts[3].Length); + Assert.Equal(12, hexParts[4].Length); + } + + // ========== V1 Time-Based Tests ========== + + [Fact(DisplayName = "RFC4122 Section 4.2.1 - V1 contains timestamp")] + public void RFC4122_V1_ContainsTimestamp() { + var beforeGen = DateTime.UtcNow; + var uuid = V1.Generate(); + var afterGen = DateTime.UtcNow; + + var info = V1.Extract(uuid); + + // Extracted timestamp should be within reasonable range + Assert.True(info.Timestamp >= beforeGen.AddSeconds(-1)); + Assert.True(info.Timestamp <= afterGen.AddSeconds(1)); + } + + [Fact(DisplayName = "RFC4122 Section 4.2.1 - V1 contains MAC address")] + public void RFC4122_V1_ContainsMacAddress() { + var uuid = V1.Generate(); + var info = V1.Extract(uuid); + + // MAC address should be 6 bytes + Assert.NotNull(info.MacAddress); + Assert.Equal(6, info.MacAddress.Length); + } + + // ========== Uniqueness Tests ========== + + [Fact(DisplayName = "RFC4122 Section 4.4 - V4 generates unique UUIDs")] + public void RFC4122_V4_Uniqueness() { + var uuids = new HashSet(); + const int count = 10000; + + for (int i = 0; i < count; i++) { + var uuid = V4.Generate(); + Assert.True(uuids.Add(uuid), $"Duplicate UUID generated: {uuid}"); + } + + Assert.Equal(count, uuids.Count); + } + + [Fact(DisplayName = "RFC4122 - V1 generates unique UUIDs")] + public void RFC4122_V1_Uniqueness() { + const int count = 1000; + + // Use batch function which properly handles timestamp incrementing + var uuids = V1.Batch(count); + var uniqueUuids = new HashSet(uuids); + + // All UUIDs should be unique when using batch generation + Assert.Equal(count, uniqueUuids.Count); + } + + // ========== Encoding Tests ========== + + [Fact(DisplayName = "RFC4122 - V3 with different encodings")] + public void RFC4122_V3_DifferentEncodings() { + var input = "test string"; + var uuidUtf8 = V3.Generate(input, Encoding.UTF8); + var uuidAscii = V3.Generate(input, Encoding.ASCII); + + // Same string with same encoding should produce same UUID + var uuidUtf8_2 = V3.Generate(input, Encoding.UTF8); + Assert.Equal(uuidUtf8, uuidUtf8_2); + + // For ASCII-compatible strings, UTF8 and ASCII should be the same + Assert.Equal(uuidUtf8, uuidAscii); + } + + [Fact(DisplayName = "RFC4122 - V5 with different encodings")] + public void RFC4122_V5_DifferentEncodings() { + var input = "test string"; + var uuidUtf8 = V5.Generate(input, Encoding.UTF8); + var uuidAscii = V5.Generate(input, Encoding.ASCII); + + // Same string with same encoding should produce same UUID + var uuidUtf8_2 = V5.Generate(input, Encoding.UTF8); + Assert.Equal(uuidUtf8, uuidUtf8_2); + + // For ASCII-compatible strings, UTF8 and ASCII should be the same + Assert.Equal(uuidUtf8, uuidAscii); + } + } diff --git a/Tests/RFC/RFC9562.cs b/Tests/RFC/RFC9562.cs index 34a717d..a1fc9c8 100644 --- a/Tests/RFC/RFC9562.cs +++ b/Tests/RFC/RFC9562.cs @@ -10,6 +10,8 @@ namespace Tests.RFC; public sealed partial class RFC9562Tests { + // ========== Test Vectors from RFC 9562 ========== + [Fact(DisplayName = "UUIDv1 RFC test vector")] public void UUIDv1_RFC_TestVector() { var uuid = new UUID("C232AB00-9414-11EC-B3C8-9F6BDECED846"); @@ -65,4 +67,306 @@ public void UUIDv8_RFC_TestVector_NameBased() { Assert.Equal("5c146b14-3c52-8afd-938a-375d0df1fbf6", uuid.ToString().ToLower()); Utility.ValidateUUID(uuid, V8.Version, V8.Variant); } + + // ========== V6 Specific Tests (RFC 9562 Section 5.6) ========== + + [Fact(DisplayName = "RFC9562 Section 5.6 - V6 is reordered Gregorian time")] + public void RFC9562_V6_ReorderedTime() { + var beforeGen = DateTime.UtcNow; + var uuid = V6.Generate(); + var afterGen = DateTime.UtcNow; + + Assert.Equal(DaanV2.UUID.Version.V6, uuid.Version); + Assert.Equal(Variant.V1, uuid.Variant); + + // V6 should contain a timestamp + var info = V6.Extract(uuid); + // V6 uses 100-nanosecond intervals, so use tighter tolerance (100ms) + Assert.True(info.Timestamp >= beforeGen.AddMilliseconds(-100)); + Assert.True(info.Timestamp <= afterGen.AddMilliseconds(100)); + } + + [Fact(DisplayName = "RFC9562 Section 5.6 - V6 UUIDs are sortable by time")] + public void RFC9562_V6_Sortable() { + var uuids = new List(); + + for (int i = 0; i < 100; i++) { + uuids.Add(V6.Generate()); + // Small delay to ensure time progression + if (i % 10 == 0) { + Thread.Sleep(1); + } + } + + // V6 UUIDs should be in ascending order when generated sequentially + var sortedUuids = uuids.OrderBy(u => u.ToString()).ToList(); + + // Check that most UUIDs maintain order (allowing for some system clock variance) + int inOrder = 0; + for (int i = 0; i < uuids.Count - 1; i++) { + if (string.Compare(uuids[i].ToString(), uuids[i + 1].ToString(), StringComparison.Ordinal) <= 0) { + inOrder++; + } + } + + // At least 90% should be in order + Assert.True(inOrder >= (uuids.Count - 1) * 0.9, + $"Expected at least 90% in order, got {inOrder}/{uuids.Count - 1}"); + } + + [Fact(DisplayName = "RFC9562 Section 5.6 - V6 uniqueness")] + public void RFC9562_V6_Uniqueness() { + const int count = 1000; + + // Use batch function which properly handles timestamp incrementing + var uuids = V6.Batch(count); + var uniqueUuids = new HashSet(uuids); + + // All UUIDs should be unique when using batch generation + Assert.Equal(count, uniqueUuids.Count); + } + + // ========== V7 Specific Tests (RFC 9562 Section 5.7) ========== + + [Fact(DisplayName = "RFC9562 Section 5.7 - V7 contains Unix Epoch timestamp")] + public void RFC9562_V7_ContainsTimestamp() { + // V7 uses Unix epoch milliseconds (RFC9562 Section 5.7) + var inputTime = DateTime.UtcNow; + var uuid = V7.Generate(inputTime); + + Assert.Equal(DaanV2.UUID.Version.V7, uuid.Version); + Assert.Equal(Variant.V1, uuid.Variant); + + // Extract timestamp and verify round-trip accuracy + var extractedTime = V7.Extract(uuid); + + // V7 only stores milliseconds, so allow up to 1ms difference + var timeDiff = Math.Abs((extractedTime - inputTime).TotalMilliseconds); + Assert.True(timeDiff < 2, + $"Time difference {timeDiff}ms is too large. Input: {inputTime}, Extracted: {extractedTime}"); + } + + [Fact(DisplayName = "RFC9562 Section 5.7 - V7 UUIDs are sortable by time")] + public void RFC9562_V7_Sortable() { + var uuids = new List<(UUID uuid, DateTime time)>(); + + // Generate UUIDs with known time gaps + for (int i = 0; i < 20; i++) { + var time = DateTime.UtcNow; + uuids.Add((V7.Generate(time), time)); + Thread.Sleep(5); // 5ms delay to ensure time progression + } + + // Verify that UUIDs generated at different times maintain time order + // Group by millisecond and verify cross-group ordering + var groups = uuids.GroupBy(x => x.time.Ticks / TimeSpan.TicksPerMillisecond).ToList(); + + // With 5ms delays, we should have multiple distinct timestamps + Assert.True(groups.Count >= 10, $"Expected at least 10 distinct timestamps, got {groups.Count}"); + + // Compare first UUID from each group - they should be in time order + for (int i = 0; i < groups.Count - 1; i++) { + var earlier = groups[i].First().uuid; + var later = groups[i + 1].First().uuid; + + // UUIDs from earlier time should compare less than UUIDs from later time + Assert.True(string.Compare(earlier.ToString(), later.ToString(), StringComparison.Ordinal) < 0, + $"UUID from earlier time should be less than UUID from later time"); + } + } + + [Fact(DisplayName = "RFC9562 Section 5.7 - V7 uniqueness")] + public void RFC9562_V7_Uniqueness() { + var uuids = new HashSet(); + const int count = 10000; + + for (int i = 0; i < count; i++) { + var uuid = V7.Generate(); + Assert.True(uuids.Add(uuid), $"Duplicate UUID generated: {uuid}"); + } + + Assert.Equal(count, uuids.Count); + } + + [Fact(DisplayName = "RFC9562 Section 5.7 - V7 millisecond precision")] + public void RFC9562_V7_MillisecondPrecision() { + // Generate multiple UUIDs within the same millisecond + var uuids = new List(); + var startTime = DateTime.UtcNow; + + // Generate rapidly to likely get some in the same millisecond + for (int i = 0; i < 100; i++) { + uuids.Add(V7.Generate()); + } + + // All should have valid timestamps + foreach (var uuid in uuids) { + var time = V7.Extract(uuid); + Assert.NotEqual(DateTime.MinValue, time); + Assert.NotEqual(DateTime.MaxValue, time); + } + + // All should be unique despite potentially being generated in same millisecond + Assert.Equal(uuids.Count, uuids.Distinct().Count()); + + // Verify that UUIDs with the same timestamp have identical timestamp bits but different random bits + var grouped = uuids.GroupBy(u => V7.ExtractUtc(u)).Where(g => g.Count() > 1).ToList(); + if (grouped.Any()) { + // Found UUIDs with same millisecond timestamp + var group = grouped.First(); + var timestampMs = group.Key; + + // All UUIDs in this group should have same timestamp value + foreach (var uuid in group) { + Assert.Equal(timestampMs, V7.ExtractUtc(uuid)); + } + + // But all UUIDs should be unique (different random portions) + Assert.Equal(group.Count(), group.Distinct().Count()); + } + } + + // ========== V8 Specific Tests (RFC 9562 Section 5.8) ========== + + [Fact(DisplayName = "RFC9562 Section 5.8 - V8 custom data support")] + public void RFC9562_V8_CustomData() { + var data = new byte[16]; + for (int i = 0; i < 16; i++) { + data[i] = (byte)i; + } + + var uuid = V8.Generate(data); + + Assert.Equal(DaanV2.UUID.Version.V8, uuid.Version); + Assert.Equal(Variant.V1, uuid.Variant); + } + + [Fact(DisplayName = "RFC9562 Section 5.8 - V8 uniqueness")] + public void RFC9562_V8_Uniqueness() { + var uuids = new HashSet(); + const int count = 10000; + + for (int i = 0; i < count; i++) { + var data = new byte[16]; + Random.Shared.NextBytes(data); + var uuid = V8.Generate(data); + Assert.True(uuids.Add(uuid), $"Duplicate UUID generated: {uuid}"); + } + + Assert.Equal(count, uuids.Count); + } + + [Fact(DisplayName = "RFC9562 Section 5.8 - V8 deterministic with same input")] + public void RFC9562_V8_Deterministic() { + var data = new byte[16]; + for (int i = 0; i < 16; i++) { + data[i] = (byte)(i * 7); + } + + var uuid1 = V8.Generate(data); + var uuid2 = V8.Generate(data); + + Assert.Equal(uuid1, uuid2); + } + + // ========== V2 Tests (DCE Security - RFC 9562 Section 5.5) ========== + + [Fact(DisplayName = "RFC9562 Section 5.5 - V2 generation and validation")] + public void RFC9562_V2_Generation() { + var uuid = V2.Generate(); + + Assert.Equal(DaanV2.UUID.Version.V2, uuid.Version); + Assert.Equal(Variant.V1, uuid.Variant); + Utility.ValidateUUID(uuid); + } + + [Fact(DisplayName = "RFC9562 Section 5.5 - V2 uniqueness")] + public void RFC9562_V2_Uniqueness() { + const int count = 50; + + // Use batch function which properly handles timestamp incrementing + // Note: V2 has design limitations - only 6 bits for local identifier variation + // So batch can only guarantee ~64 unique UUIDs before timestamp increment + var uuids = V2.Batch(count); + var uniqueUuids = new HashSet(uuids); + + // All UUIDs should be unique when using batch generation within limit + Assert.Equal(count, uniqueUuids.Count); + } + + // ========== Round-Trip Tests for New Versions ========== + + [Fact(DisplayName = "RFC9562 - V6 round-trip consistency")] + public void RFC9562_V6_RoundTrip() { + var original = V6.Generate(); + var str = original.ToString(); + var parsed = new UUID(str); + + Assert.Equal(original, parsed); + Assert.Equal(original.Version, parsed.Version); + Assert.Equal(original.Variant, parsed.Variant); + } + + [Fact(DisplayName = "RFC9562 - V7 round-trip consistency")] + public void RFC9562_V7_RoundTrip() { + var original = V7.Generate(); + var str = original.ToString(); + var parsed = new UUID(str); + + Assert.Equal(original, parsed); + Assert.Equal(original.Version, parsed.Version); + Assert.Equal(original.Variant, parsed.Variant); + } + + [Fact(DisplayName = "RFC9562 - V8 round-trip consistency")] + public void RFC9562_V8_RoundTrip() { + var data = new byte[16]; + Random.Shared.NextBytes(data); + var original = V8.Generate(data); + var str = original.ToString(); + var parsed = new UUID(str); + + Assert.Equal(original, parsed); + Assert.Equal(original.Version, parsed.Version); + Assert.Equal(original.Variant, parsed.Variant); + } + + // ========== Compatibility Tests ========== + + [Fact(DisplayName = "RFC9562 - V1 and V6 timestamp relationship")] + public void RFC9562_V1_V6_TimestampCompatibility() { + // V6 is a reordering of V1 timestamps + // Both should extract similar timestamps when generated at the same time + var v1uuid = V1.Generate(); + Thread.Sleep(10); // Small delay + var v6uuid = V6.Generate(); + + var v1Info = V1.Extract(v1uuid); + var v6Info = V6.Extract(v6uuid); + + // Both should have valid timestamps + Assert.NotEqual(DateTime.MinValue, v1Info.Timestamp); + Assert.NotEqual(DateTime.MinValue, v6Info.Timestamp); + + // V6 should be after V1 (with some tolerance) + Assert.True(v6Info.Timestamp >= v1Info.Timestamp.AddMilliseconds(-100)); + } + + [Fact(DisplayName = "RFC9562 - Nil UUID special case")] + public void RFC9562_NilUUID() { + var nilUuid = UUID.Zero; + + Assert.Equal("00000000-0000-0000-0000-000000000000", nilUuid.ToString().ToLower()); + + // Nil UUID has all zero bits, so version bits are also zero + // The implementation may not expose Version.V0, so we just verify the string format + Utility.ValidateUUID(nilUuid); + } + + [Fact(DisplayName = "RFC9562 - Max UUID special case")] + public void RFC9562_MaxUUID() { + var maxUuid = UUID.Max; + + Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", maxUuid.ToString().ToLower()); + } }