Files
platform/projection/hashing_test.go
T
2026-06-29 13:17:43 +02:00

115 lines
3.5 KiB
Go

package projection
import (
"path/filepath"
"strings"
"testing"
)
// TestKeyHashing_LongKeyRoundTrips — a key well over the 32-byte raw ceiling
// (a query string / page path) is accepted in hashed mode and Get finds it.
func TestKeyHashing_LongKeyRoundTrips(t *testing.T) {
dir := t.TempDir()
longKey := "/kategori/" + strings.Repeat("hem-hushall-tradgard/", 5) + "?q=" + strings.Repeat("x", 80)
if len(longKey) <= maxSKULen {
t.Fatalf("test bug: key is only %d bytes, not over the ceiling", len(longKey))
}
w := NewSnapshotWriter[tProj]().WithKeyHashing()
// The value carries the original key — the id/sku stays the link even though
// the index key is a digest.
if err := w.Append(longKey, tProj{SKU: longKey, PriceIncVat: 42}); err != nil {
t.Fatalf("Append long key in hashed mode: %v", err)
}
if _, err := w.Write(dir, "hashed.bin"); err != nil {
t.Fatal(err)
}
r, err := ReadSnapshotFile[tProj](filepath.Join(dir, "hashed.bin"))
if err != nil {
t.Fatal(err)
}
defer r.Close()
got, ok := r.Get(longKey)
if !ok {
t.Fatal("Get(longKey) miss — hashed lookup did not match the stored digest")
}
if got.PriceIncVat != 42 || got.SKU != longKey {
t.Fatalf("got %+v, want price 42 and original key in value", got)
}
// Scan yields the digest, not the original — caller recovers it from the value.
r.Scan(func(key string, v tProj) bool {
if key == longKey {
t.Error("Scan key should be the digest in hashed mode, got the original")
}
if len(key) != 32 {
t.Errorf("digest key len = %d, want 32 hex chars", len(key))
}
if v.SKU != longKey {
t.Errorf("value lost the original key: %q", v.SKU)
}
return true
})
}
// TestKeyHashing_RawModeStillRejectsLongKeys — default (raw) mode is unchanged:
// an over-long key still errors, so catalog's contract is untouched.
func TestKeyHashing_RawModeStillRejectsLongKeys(t *testing.T) {
w := NewSnapshotWriter[tProj]() // raw
long := strings.Repeat("x", 33)
if err := w.Append(long, tProj{SKU: long}); err == nil {
t.Fatal("raw mode should still reject a 33-byte key")
}
}
// TestKeyHashing_RawFileHeaderUnchanged — a raw-mode file sets keyEnc=0 at
// offset 12, i.e. byte-identical header to a pre-hashing v2 file. Guards the
// back-compat claim for catalog's existing .bin files.
func TestKeyHashing_RawFileHeaderUnchanged(t *testing.T) {
dir := t.TempDir()
w := NewSnapshotWriter[tProj]()
if err := appendP(w, makeProjection("A", 1)); err != nil {
t.Fatal(err)
}
if _, err := w.Write(dir, "raw.bin"); err != nil {
t.Fatal(err)
}
r, err := ReadSnapshotFile[tProj](filepath.Join(dir, "raw.bin"))
if err != nil {
t.Fatal(err)
}
defer r.Close()
if r.raw.keyEnc != keyEncRaw {
t.Fatalf("raw file keyEnc = %d, want 0", r.raw.keyEnc)
}
if _, ok := r.Get("A"); !ok {
t.Error("raw-mode Get still works")
}
}
// TestKeyHashing_UnknownEncodingRejected — a header carrying an unknown key
// encoding is rejected rather than silently mis-looked-up.
func TestKeyHashing_UnknownEncodingRejected(t *testing.T) {
dir := t.TempDir()
w := NewSnapshotWriter[tProj]()
if err := appendP(w, makeProjection("A", 1)); err != nil {
t.Fatal(err)
}
path := filepath.Join(dir, "tamper.bin")
if _, err := w.Write(dir, "tamper.bin"); err != nil {
t.Fatal(err)
}
data, err := os2ReadFile(path)
if err != nil {
t.Fatal(err)
}
data[12] = 0x09 // keyEnc = 9, unknown
if err := os2WriteFile(path, data, 0o644); err != nil {
t.Fatal(err)
}
if _, err := ReadSnapshotFile[tProj](path); err == nil {
t.Fatal("unknown key encoding should be rejected")
}
}