|
| 1 | +package hash |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "crypto/md5" |
| 6 | + "encoding/hex" |
| 7 | + "encoding/json" |
| 8 | + "testing" |
| 9 | + |
| 10 | + "github.com/google/uuid" |
| 11 | +) |
| 12 | + |
| 13 | +func TestJSONMD5Hash_ReturnsHexEncoded(t *testing.T) { |
| 14 | + // Hex encoding of an MD5 digest is always 32 characters. |
| 15 | + h, err := JSONMD5Hash("hello") |
| 16 | + if err != nil { |
| 17 | + t.Fatalf("unexpected error: %v", err) |
| 18 | + } |
| 19 | + if len(h) != 32 { |
| 20 | + t.Fatalf("expected 32-char hex hash, got %d chars: %q", len(h), h) |
| 21 | + } |
| 22 | + if _, err := hex.DecodeString(h); err != nil { |
| 23 | + t.Fatalf("hash is not valid hex: %v", err) |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +func TestJSONMD5Hash_Deterministic(t *testing.T) { |
| 28 | + a, _ := JSONMD5Hash(map[string]string{"k": "v"}) |
| 29 | + b, _ := JSONMD5Hash(map[string]string{"k": "v"}) |
| 30 | + if a != b { |
| 31 | + t.Fatalf("expected deterministic hash, got %q vs %q", a, b) |
| 32 | + } |
| 33 | +} |
| 34 | + |
| 35 | +func TestDeterministicUUID_UsesRawBytes(t *testing.T) { |
| 36 | + // Regression for a bug where DeterministicUUID fed the *hex-encoded* |
| 37 | + // JSONMD5Hash string into uuid.FromBytes, producing UUIDs whose bytes |
| 38 | + // were the ASCII codes of hex digits (e.g. 30663964-3638-3061-... where |
| 39 | + // 0x30='0', 0x66='f', 0x39='9', 0x64='d'). The correct behavior is to |
| 40 | + // use the raw 16-byte md5 digest as the UUID bytes. |
| 41 | + |
| 42 | + seed := "test-seed" |
| 43 | + got, err := DeterministicUUID(seed) |
| 44 | + if err != nil { |
| 45 | + t.Fatalf("unexpected error: %v", err) |
| 46 | + } |
| 47 | + |
| 48 | + data, _ := json.Marshal(seed) |
| 49 | + sum := md5.Sum(data) |
| 50 | + // The UUID bytes must equal the raw md5 bytes. |
| 51 | + for i := 0; i < 16; i++ { |
| 52 | + if got[i] != sum[i] { |
| 53 | + t.Fatalf("UUID byte %d: want %#x, got %#x", i, sum[i], got[i]) |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + // Also assert the UUID is NOT the ASCII-hex-encoded variant of the hex |
| 58 | + // representation of the md5, which is what the old buggy code produced. |
| 59 | + hexStr := hex.EncodeToString(sum[:]) |
| 60 | + var bogus [16]byte |
| 61 | + copy(bogus[:], hexStr[:16]) |
| 62 | + if got == bogus { |
| 63 | + t.Fatal("DeterministicUUID regressed: still uses ASCII hex bytes as UUID bytes") |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +func TestDeterministicUUID_Deterministic(t *testing.T) { |
| 68 | + a, _ := DeterministicUUID("same-seed") |
| 69 | + b, _ := DeterministicUUID("same-seed") |
| 70 | + if a != b { |
| 71 | + t.Fatalf("expected deterministic UUID, got %q vs %q", a, b) |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +func TestDeterministicUUID_DifferentSeedsProduceDifferentUUIDs(t *testing.T) { |
| 76 | + a, _ := DeterministicUUID("seed-one") |
| 77 | + b, _ := DeterministicUUID("seed-two") |
| 78 | + if a == b { |
| 79 | + t.Fatalf("expected distinct UUIDs for distinct seeds, got %q twice", a) |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +const passthroughUUIDStr = "550e8400-e29b-41d4-a716-446655440000" |
| 84 | + |
| 85 | +func TestDeterministicUUID_PassesThroughValidUUIDString(t *testing.T) { |
| 86 | + got, err := DeterministicUUID(passthroughUUIDStr) |
| 87 | + if err != nil { |
| 88 | + t.Fatalf("unexpected error: %v", err) |
| 89 | + } |
| 90 | + if got.String() != passthroughUUIDStr { |
| 91 | + t.Fatalf("expected passthrough %q, got %q", passthroughUUIDStr, got.String()) |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +func TestDeterministicUUID_PassesThroughUUIDValue(t *testing.T) { |
| 96 | + in := uuid.MustParse(passthroughUUIDStr) |
| 97 | + got, err := DeterministicUUID(in) |
| 98 | + if err != nil { |
| 99 | + t.Fatalf("unexpected error: %v", err) |
| 100 | + } |
| 101 | + if got != in { |
| 102 | + t.Fatalf("expected passthrough %q, got %q", in, got) |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +func TestDeterministicUUID_PassesThroughUUIDPointer(t *testing.T) { |
| 107 | + in := uuid.MustParse(passthroughUUIDStr) |
| 108 | + got, err := DeterministicUUID(&in) |
| 109 | + if err != nil { |
| 110 | + t.Fatalf("unexpected error: %v", err) |
| 111 | + } |
| 112 | + if got != in { |
| 113 | + t.Fatalf("expected passthrough %q, got %q", in, got) |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +func TestDeterministicUUID_PassesThroughUUIDBytes(t *testing.T) { |
| 118 | + in := uuid.MustParse(passthroughUUIDStr) |
| 119 | + |
| 120 | + gotArr, err := DeterministicUUID([16]byte(in)) |
| 121 | + if err != nil { |
| 122 | + t.Fatalf("unexpected error ([16]byte): %v", err) |
| 123 | + } |
| 124 | + if gotArr != in { |
| 125 | + t.Fatalf("[16]byte passthrough: want %q, got %q", in, gotArr) |
| 126 | + } |
| 127 | + |
| 128 | + gotSlice, err := DeterministicUUID(in[:]) |
| 129 | + if err != nil { |
| 130 | + t.Fatalf("unexpected error ([]byte): %v", err) |
| 131 | + } |
| 132 | + if gotSlice != in { |
| 133 | + t.Fatalf("[]byte passthrough: want %q, got %q", in, gotSlice) |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +func TestDeterministicUUID_PassesThroughNilUUID(t *testing.T) { |
| 138 | + const nilStr = "00000000-0000-0000-0000-000000000000" |
| 139 | + |
| 140 | + cases := []struct { |
| 141 | + name string |
| 142 | + in any |
| 143 | + }{ |
| 144 | + {"uuid.Nil value", uuid.Nil}, |
| 145 | + {"nil string", nilStr}, |
| 146 | + {"zero [16]byte", [16]byte{}}, |
| 147 | + } |
| 148 | + for _, tc := range cases { |
| 149 | + got, err := DeterministicUUID(tc.in) |
| 150 | + if err != nil { |
| 151 | + t.Fatalf("%s: unexpected error: %v", tc.name, err) |
| 152 | + } |
| 153 | + if got != uuid.Nil { |
| 154 | + t.Fatalf("%s: expected uuid.Nil passthrough, got %q", tc.name, got) |
| 155 | + } |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +func TestDeterministicUUID_SingleElementSliceIsHashed(t *testing.T) { |
| 160 | + in := []string{passthroughUUIDStr} |
| 161 | + got, err := DeterministicUUID(in) |
| 162 | + if err != nil { |
| 163 | + t.Fatalf("unexpected error: %v", err) |
| 164 | + } |
| 165 | + if got.String() == passthroughUUIDStr { |
| 166 | + t.Fatal("single-element slice must be treated as a composite and hashed, not unwrapped") |
| 167 | + } |
| 168 | + if got == uuid.Nil { |
| 169 | + t.Fatal("hashed composite should not be uuid.Nil") |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +func TestDeterministicUUID_NonUUIDStringStillHashes(t *testing.T) { |
| 174 | + got, err := DeterministicUUID("test-seed") |
| 175 | + if err != nil { |
| 176 | + t.Fatalf("unexpected error: %v", err) |
| 177 | + } |
| 178 | + |
| 179 | + data, _ := json.Marshal("test-seed") |
| 180 | + sum := md5.Sum(data) |
| 181 | + if !bytes.Equal(got[:], sum[:]) { |
| 182 | + t.Fatalf("non-UUID strings must still hash; want %x, got %x", sum, got[:]) |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +func TestDeterministicUUID_ShortByteSliceStillHashes(t *testing.T) { |
| 187 | + in := []byte{1, 2, 3} |
| 188 | + got, err := DeterministicUUID(in) |
| 189 | + if err != nil { |
| 190 | + t.Fatalf("unexpected error: %v", err) |
| 191 | + } |
| 192 | + |
| 193 | + data, _ := json.Marshal(in) |
| 194 | + sum := md5.Sum(data) |
| 195 | + if !bytes.Equal(got[:], sum[:]) { |
| 196 | + t.Fatalf("len != 16 []byte must hash via JSON-MD5; want %x, got %x", sum, got[:]) |
| 197 | + } |
| 198 | +} |
0 commit comments