Files
doormile_backend/controllers/cxIdentifierScramble_test.go

282 lines
9.9 KiB
Go

package controllers
import (
"fmt"
"testing"
)
// The scrambling exists to remove an information leak, but the property that
// MUST survive it is uniqueness: trackingno and bookingno are UNIQUE columns,
// and a collision is a rider standing at a door unable to complete a pickup.
// Every test here is ultimately about that.
// No two sequence values may ever produce the same tracking number. Checked
// over a large contiguous run, which is exactly the shape real traffic takes.
func TestTrackingScrambleIsCollisionFree(t *testing.T) {
const sample = 200_000
seen := make(map[uint64]int64, sample)
for seq := int64(cxTrackingBase); seq < cxTrackingBase+sample; seq++ {
got, ok := cxScrambledTracking(seq)
if !ok {
t.Fatalf("seq %d reported out of range inside the domain", seq)
}
if prev, dup := seen[got]; dup {
t.Fatalf("COLLISION: seq %d and seq %d both produced %d — "+
"the UNIQUE constraint would reject the second booking", prev, seq, got)
}
seen[got] = seq
}
if len(seen) != sample {
t.Errorf("produced %d distinct numbers from %d inputs", len(seen), sample)
}
}
func TestBookingScrambleIsCollisionFree(t *testing.T) {
// The booking domain is only 900,000, so the whole thing is checkable —
// this is an exhaustive proof of bijectivity, not a sample.
seen := make(map[uint64]int64, cxBookingDomain)
for seq := int64(cxBookingBase); seq < cxBookingBase+cxBookingDomain; seq++ {
got, ok := cxScrambledBooking(seq)
if !ok {
t.Fatalf("seq %d reported out of range inside the domain", seq)
}
if prev, dup := seen[got]; dup {
t.Fatalf("COLLISION: seq %d and seq %d both produced %d", prev, seq, got)
}
seen[got] = seq
}
if len(seen) != cxBookingDomain {
t.Fatalf("the permutation is not a bijection: %d distinct outputs from %d inputs",
len(seen), cxBookingDomain)
}
}
// Output must stay inside the digit range, or the format silently changes
// width and every label, column and deep link that assumed it breaks.
func TestScrambledIdentifiersKeepTheirWidth(t *testing.T) {
for _, seq := range []int64{
cxTrackingBase,
cxTrackingBase + 1,
cxTrackingBase + 12_345,
cxTrackingBase + cxTrackingDomain - 1,
} {
got, ok := cxScrambledTracking(seq)
if !ok {
t.Fatalf("seq %d out of range", seq)
}
if got < cxTrackingBase || got > 99_999_999 {
t.Errorf("seq %d produced %d, outside the eight-digit range", seq, got)
}
if formatted := fmt.Sprintf("DMX%08d", got); len(formatted) != 11 {
t.Errorf("formatted as %q (%d chars), want 11", formatted, len(formatted))
}
}
for _, seq := range []int64{
cxBookingBase,
cxBookingBase + 1,
cxBookingBase + cxBookingDomain - 1,
} {
got, ok := cxScrambledBooking(seq)
if !ok {
t.Fatalf("seq %d out of range", seq)
}
if got < cxBookingBase || got > 999_999 {
t.Errorf("seq %d produced %d, outside the six-digit range", seq, got)
}
if formatted := fmt.Sprintf("DM-%06d", got); len(formatted) != 9 {
t.Errorf("formatted as %q (%d chars), want 9", formatted, len(formatted))
}
}
}
// The point of the whole exercise: consecutive sequence values must NOT produce
// adjacent identifiers. This is the leak being closed.
func TestConsecutiveSequenceValuesAreNotAdjacent(t *testing.T) {
const run = 500
var previous uint64
adjacent := 0
for i := 0; i < run; i++ {
got, _ := cxScrambledTracking(int64(cxTrackingBase + i))
if i > 0 {
diff := int64(got) - int64(previous)
if diff < 0 {
diff = -diff
}
if diff < 100 {
adjacent++
}
}
previous = got
}
// In a well-scattered 90,000,000-wide range, landing within 100 of the
// previous value should essentially never happen.
if adjacent > 2 {
t.Errorf("%d of %d consecutive pairs landed within 100 of each other — "+
"the identifiers are still walkable", adjacent, run-1)
}
}
// A multi-destination pickup hands ONE customer several consecutive sequence
// values at once. If the mapping were linear — multiply by a coprime, the
// obvious one-line trick — the differences between those tracking numbers would
// all equal the multiplier, and that single booking would hand over the key to
// the whole range. This is the test that rejects that design.
func TestOneBookingDoesNotLeakTheMapping(t *testing.T) {
// Three orders minted back to back, as a three-destination pickup would.
a, _ := cxScrambledTracking(cxTrackingBase + 5000)
b, _ := cxScrambledTracking(cxTrackingBase + 5001)
c, _ := cxScrambledTracking(cxTrackingBase + 5002)
d1 := int64(b) - int64(a)
d2 := int64(c) - int64(b)
if d1 == d2 {
t.Fatalf("consecutive differences are identical (%d) — the mapping is "+
"linear, so one multi-destination booking reveals it and the whole "+
"range becomes enumerable", d1)
}
// And knowing two neighbours must not predict the third.
if int64(c) == int64(b)+d1 {
t.Error("the third identifier is predictable from the first two")
}
}
// The mapping is deterministic — the same sequence value always yields the same
// identifier. It is computed at insert time and stored, so this matters only
// for reasoning and tests, but a non-deterministic mapping would mean the
// scrambling depended on something it should not.
func TestScramblingIsDeterministic(t *testing.T) {
for _, seq := range []int64{cxTrackingBase, cxTrackingBase + 99, cxTrackingBase + 123_456} {
first, _ := cxScrambledTracking(seq)
for i := 0; i < 5; i++ {
again, _ := cxScrambledTracking(seq)
if again != first {
t.Fatalf("seq %d produced %d then %d", seq, first, again)
}
}
}
}
// Past the fixed-width range the caller must be told, so it can let the
// identifier grow a digit rather than wrap onto one already issued. Wrapping
// would be a duplicate, and a duplicate is a failed booking.
func TestExhaustedDomainIsReportedNotWrapped(t *testing.T) {
if _, ok := cxScrambledTracking(cxTrackingBase + cxTrackingDomain); ok {
t.Error("the first sequence value past the tracking domain was accepted — " +
"it would wrap onto an identifier already issued")
}
if _, ok := cxScrambledBooking(cxBookingBase + cxBookingDomain); ok {
t.Error("the first sequence value past the booking domain was accepted")
}
// And the generator falls back to plain sequential formatting there, which
// grows a digit rather than colliding.
if got := fmt.Sprintf("DM-%06d", cxBookingBase+cxBookingDomain); len(got) != 10 {
t.Errorf("the overflow reference formats as %q; it should simply grow a digit", got)
}
}
// A value below the sequence start is not a valid index and must be refused
// rather than producing a negative or wrapped result.
func TestBelowBaseIsRefused(t *testing.T) {
if _, ok := cxScrambledTracking(0); ok {
t.Error("seq 0 accepted for tracking")
}
if _, ok := cxScrambledBooking(cxBookingBase - 1); ok {
t.Error("a sequence value below the booking base was accepted")
}
}
// The Feistel itself is a permutation over the full power-of-two space. This is
// the property everything else rests on, so it is checked directly rather than
// only through its callers.
func TestFeistelIsAPermutation(t *testing.T) {
const halfBits = 8 // a 16-bit space, small enough to check exhaustively
full := uint64(1) << (2 * halfBits)
seen := make(map[uint64]uint64, full)
for x := uint64(0); x < full; x++ {
y := cxFeistelEncrypt(x, halfBits, cxScrambleKey)
if y >= full {
t.Fatalf("encrypt(%d) = %d, outside the %d-wide space", x, y, full)
}
if prev, dup := seen[y]; dup {
t.Fatalf("not a permutation: %d and %d both map to %d", prev, x, y)
}
seen[y] = x
}
if uint64(len(seen)) != full {
t.Fatalf("covered %d of %d values", len(seen), full)
}
}
// A different key must produce a different permutation — otherwise the key is
// not actually keying anything.
func TestKeyChangesThePermutation(t *testing.T) {
const halfBits = 8
differences := 0
for x := uint64(0); x < 256; x++ {
if cxFeistelEncrypt(x, halfBits, []byte("key-one")) !=
cxFeistelEncrypt(x, halfBits, []byte("key-two")) {
differences++
}
}
if differences < 250 {
t.Errorf("only %d of 256 values differed between keys — the key has "+
"little effect on the mapping", differences)
}
}
// Every surface — customer app, miler app, admin console, hub console — reads
// the SAME column, so format consistency is structural: one generator, one
// stored value. These assert the generators themselves produce the documented
// shape, including on the fallback path that runs when the sequence cannot be
// read (no database in a test, which is exactly what exercises it here).
func TestGeneratorsProduceTheDocumentedFormat(t *testing.T) {
for i := 0; i < 50; i++ {
booking := generateBookingNo()
if len(booking) < 9 || booking[:3] != "DM-" {
t.Fatalf("generateBookingNo() = %q, want DM- followed by at least six digits", booking)
}
for _, r := range booking[3:] {
if r < '0' || r > '9' {
t.Fatalf("generateBookingNo() = %q — the part after DM- must be digits only", booking)
}
}
tracking := generateTrackingNo()
if len(tracking) < 11 || tracking[:3] != "DMX" {
t.Fatalf("generateTrackingNo() = %q, want DMX followed by at least eight digits", tracking)
}
for _, r := range tracking[3:] {
if r < '0' || r > '9' {
t.Fatalf("generateTrackingNo() = %q — the part after DMX must be digits only", tracking)
}
}
// A tracking number must never be mistakeable for a booking reference:
// the assistant and the consoles tell them apart by prefix alone.
if tracking[:3] == "DM-" {
t.Fatalf("tracking number %q collides with the booking reference prefix", tracking)
}
}
}
// The fallback path must never emit a short number that formats with leading
// zeros — DM-000042 reads as a broken reference, and a padded id is a support
// call.
func TestFallbackNumberNeverGoesShort(t *testing.T) {
for i := 0; i < 200; i++ {
if n := fallbackNumber(6); n < 100_000 || n > 999_999 {
t.Fatalf("fallbackNumber(6) = %d, outside the six-digit range", n)
}
if n := fallbackNumber(8); n < 10_000_000 || n > 99_999_999 {
t.Fatalf("fallbackNumber(8) = %d, outside the eight-digit range", n)
}
}
}