282 lines
9.9 KiB
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)
|
|
}
|
|
}
|
|
}
|