|
| 1 | +package client |
| 2 | + |
| 3 | +import ( |
| 4 | + "regexp" |
| 5 | + "testing" |
| 6 | + "time" |
| 7 | + |
| 8 | + "github.com/rs/xid" |
| 9 | + "github.com/stretchr/testify/require" |
| 10 | +) |
| 11 | + |
| 12 | +// xidAlphabet matches the server-side validator (see pkg/server/util.go). |
| 13 | +var xidAlphabet = regexp.MustCompile(`^[0-9a-v]{20}$`) |
| 14 | + |
| 15 | +func TestAnonymousCorrelationIDFormat(t *testing.T) { |
| 16 | + id, err := newAnonymousCorrelationID() |
| 17 | + require.NoError(t, err) |
| 18 | + require.Len(t, id, 20) |
| 19 | + require.Regexp(t, xidAlphabet, id, "must match server xid alphabet validator") |
| 20 | + |
| 21 | + parsed, err := xid.FromString(id) |
| 22 | + require.NoError(t, err, "must remain parseable as xid") |
| 23 | + require.WithinDuration(t, time.Now(), parsed.Time(), 5*time.Second, |
| 24 | + "timestamp prefix must encode the current time") |
| 25 | +} |
| 26 | + |
| 27 | +// TestAnonymousCorrelationIDNotFingerprinted asserts that the machine and pid |
| 28 | +// bytes are randomised: xid.New() returns the same machine+pid for every call |
| 29 | +// in the same process; newAnonymousCorrelationID() must not. |
| 30 | +func TestAnonymousCorrelationIDNotFingerprinted(t *testing.T) { |
| 31 | + const samples = 64 |
| 32 | + |
| 33 | + machineSet := make(map[string]struct{}, samples) |
| 34 | + pidSet := make(map[uint16]struct{}, samples) |
| 35 | + |
| 36 | + for range samples { |
| 37 | + raw, err := newAnonymousCorrelationID() |
| 38 | + require.NoError(t, err) |
| 39 | + id, err := xid.FromString(raw) |
| 40 | + require.NoError(t, err) |
| 41 | + machineSet[string(id.Machine())] = struct{}{} |
| 42 | + pidSet[id.Pid()] = struct{}{} |
| 43 | + } |
| 44 | + |
| 45 | + // With 64 samples and full randomness, the probability of getting a |
| 46 | + // single repeated 3-byte machine value is ~64^2 / 2^25 ≈ 1.2e-4. A |
| 47 | + // fingerprinted id would collapse to a single value, so even a weak |
| 48 | + // lower bound here catches the regression. |
| 49 | + require.Greater(t, len(machineSet), samples/2, |
| 50 | + "machine bytes must be randomised across calls") |
| 51 | + require.Greater(t, len(pidSet), samples/2, |
| 52 | + "pid bytes must be randomised across calls") |
| 53 | +} |
| 54 | + |
| 55 | +func TestAnonymousCorrelationIDDistinctFromXidNew(t *testing.T) { |
| 56 | + // xid.New() inherits a stable machine fingerprint per process; the |
| 57 | + // anonymous helper must not. |
| 58 | + stable := xid.New().Machine() |
| 59 | + collisions := 0 |
| 60 | + const samples = 32 |
| 61 | + for range samples { |
| 62 | + raw, err := newAnonymousCorrelationID() |
| 63 | + require.NoError(t, err) |
| 64 | + id, err := xid.FromString(raw) |
| 65 | + require.NoError(t, err) |
| 66 | + if string(id.Machine()) == string(stable) { |
| 67 | + collisions++ |
| 68 | + } |
| 69 | + } |
| 70 | + // A truly fingerprinted helper would collide on every sample. Allow a |
| 71 | + // generous threshold so the test stays stable against random chance |
| 72 | + // (expected collisions ≈ samples / 2^24, i.e. effectively zero). |
| 73 | + require.Less(t, collisions, samples/4, |
| 74 | + "anonymous helper must not reuse xid.New()'s machine fingerprint") |
| 75 | +} |
0 commit comments