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") } }