package fejkdata import ( "encoding/base64" "regexp" "strconv" "testing" ) func TestBuiltinIDGenerators(t *testing.T) { cases := []struct { name string tmpl string re *regexp.Regexp }{ {"uuid v7", `{"format":"{uuid()}"}`, regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)}, {"ulid", `{"format":"{ulid()}"}`, regexp.MustCompile(`^[0-7][0-9A-HJKMNP-TV-Z]{25}$`)}, {"nanoid", `{"format":"{nanoid(21)}"}`, regexp.MustCompile(`^[A-Za-z0-9_-]{21}$`)}, {"hex", `{"format":"{hex(16)}"}`, regexp.MustCompile(`^[0-9a-f]{16}$`)}, } f := engine(1) for _, c := range cases { seen := map[string]bool{} for i := 0; i < 200; i++ { got := mustRender(t, f, c.tmpl) if !c.re.MatchString(got) { t.Fatalf("%s = %q, want %s", c.name, got, c.re) } seen[got] = true } if len(seen) < 190 { t.Fatalf("%s not varied: %d uniques in 200", c.name, len(seen)) } } } // TestBuiltinGeneratorsReproducible pins the determinism guardrail: every // generator draws only from the seeded rng, so same seed -> same value. func TestBuiltinGeneratorsReproducible(t *testing.T) { for _, tmpl := range []string{ `{"format":"{uuid()}"}`, `{"format":"{ulid()}"}`, `{"format":"{nanoid(12)}"}`, `{"format":"{int(1,1000000)}"}`, `{"format":"{float(0,1,6)}"}`, `{"format":"{base64(12)}"}`, `{"format":"{iban(SE)}"}`, } { if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b { t.Fatalf("%s not reproducible: %q != %q", tmpl, a, b) } } } func TestBuiltinIntInclusiveRange(t *testing.T) { f := engine(1) lo, hi := false, false for i := 0; i < 1000; i++ { n, err := strconv.Atoi(mustRender(t, f, `{"format":"{int(3,7)}"}`)) if err != nil || n < 3 || n > 7 { t.Fatalf("int(3,7) = %d (err %v), out of range", n, err) } lo, hi = lo || n == 3, hi || n == 7 } if !lo || !hi { t.Fatalf("int(3,7) never hit a bound: lo=%v hi=%v (bounds must be inclusive)", lo, hi) } } func TestBuiltinFloat(t *testing.T) { f, re := engine(1), regexp.MustCompile(`^[12]\.\d{3}$`) for i := 0; i < 200; i++ { if got := mustRender(t, f, `{"format":"{float(1,2,3)}"}`); !re.MatchString(got) { t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got) } } } func TestBuiltinBase64(t *testing.T) { got := mustRender(t, engine(1), `{"format":"{base64(9)}"}`) if b, err := base64.StdEncoding.DecodeString(got); err != nil || len(b) != 9 { t.Fatalf("base64(9) = %q decodes to %d bytes (err %v), want 9", got, len(b), err) } } // TestBuiltinChecksums fixes the payload with escapes and compares to a // hand-computed check, like the luhn test. mod-11 emits X when it would be 10. func TestBuiltinChecksums(t *testing.T) { cases := map[string]string{ `{"format":"#1#2#3#4#5#6#7#8{mod11()}"}`: "123456785", // weights 2..7 from the right `{"format":"#6{mod11()}"}`: "6X", // remainder 10 -> X `{"format":"#4#0#0#6#3#8#1#3#3#3#9#3{ean()}"}`: "4006381333931", // EAN-13 (= ISBN-13) check digit } f := engine(1) for tmpl, want := range cases { if got := mustRender(t, f, tmpl); got != want { t.Fatalf("%s = %q, want %q", tmpl, got, want) } } } // TestBuiltinSeqPerSession pins seq's contract: a counter from 1, advancing on // each call, named counters independent, and the whole thing scoped to one faker // (session) so a fresh faker restarts at 1. func TestBuiltinSeqPerSession(t *testing.T) { f := engine(1) for i := 1; i <= 5; i++ { if got := mustRender(t, f, `{"format":"{seq()}"}`); got != strconv.Itoa(i) { t.Fatalf("seq call %d = %q, want %d", i, got, i) } } if got := mustRender(t, f, `{"format":"{seq(orders)}"}`); got != "1" { t.Fatalf("named seq(orders) = %q, want its own count from 1", got) } if got := mustRender(t, f, `{"format":"{seq()}"}`); got != "6" { t.Fatalf("default seq after the named one = %q, want 6 (counters are independent)", got) } // repeat drives one counter forward across its renders... if got := mustRender(t, engine(2), `{"format":"{seq()}","repeat":3,"separator":","}`); got != "1,2,3" { t.Fatalf("repeat seq = %q, want 1,2,3", got) } // ...and a fresh session restarts from 1. if got := mustRender(t, engine(2), `{"format":"{seq()}"}`); got != "1" { t.Fatalf("new session seq = %q, want 1", got) } } // TestBuiltinArgLimits pins the New-time guards that keep a fat-fingered or // overflowing argument from panicking or OOM-ing at render. Each must be rejected // at compile, not detonate when the template is drawn. func TestBuiltinArgLimits(t *testing.T) { bad := []string{ `{"format":"{int(-9223372036854775808,9223372036854775807)}"}`, // span overflows int -> IntN panic `{"format":"{hex(2000000000)}"}`, // ~2 GB string `{"format":"{nanoid(2000000000)}"}`, `{"format":"{base64(2000000000)}"}`, `{"format":"{float(-1e308,1e308,2)}"}`, // span is +Inf `{"format":"{float(0,1,100000)}"}`, // decimals -> huge string `{"format":"{calc(1+1,100000)}"}`, // calc decimals -> huge string `{"format":"{x}","x":["1"],"repeat":2000000000}`, // repeat -> multi-GB join } for _, tmpl := range bad { if _, err := compile(parse(t, tmpl)); err == nil { t.Errorf("compile(%s) = nil error, want a limit rejection", tmpl) } } // Ordinary values still compile. if _, err := compile(parse(t, `{"format":"{hex(16)} {int(-5,5)} {float(0,1,3)} {calc(1+1,2)}"}`)); err != nil { t.Errorf("compile of normal args failed: %v", err) } } func TestBuiltinIBAN(t *testing.T) { wantLen := map[string]int{"SE": 24, "DE": 22, "NO": 15} f := engine(1) for cc, n := range wantLen { tmpl := `{"format":"{iban(` + cc + `)}"}` for i := 0; i < 100; i++ { got := mustRender(t, f, tmpl) if got[:2] != cc || len(got) != n { t.Fatalf("iban(%s) = %q, want %d chars prefixed %s", cc, got, n, cc) } if !ibanValid(got) { t.Fatalf("iban(%s) = %q fails the mod-97 check", cc, got) } } } } // ibanValid checks the IBAN mod-97 rule independently of the generator: move the // first four chars to the end, map letters A-Z to 10-35, the number mod 97 == 1. func ibanValid(s string) bool { r := s[4:] + s[:4] rem := 0 for i := 0; i < len(r); i++ { switch c := r[i]; { case c >= '0' && c <= '9': rem = (rem*10 + int(c-'0')) % 97 case c >= 'A' && c <= 'Z': rem = (rem*100 + int(c-'A') + 10) % 97 default: return false } } return rem == 1 } // TestRegistryShapes pins the builtin contract compileOps relies on: every entry // supplies prep, and args parsed at compile only behind a check. func TestRegistryShapes(t *testing.T) { for name, b := range builtins { if b.prep == nil { t.Errorf("builtin %q has no prep: compileOps would call a nil func", name) } if b.arity != 0 && b.check == nil { t.Errorf("builtin %q parses args in prep with no check", name) } } } // TestArgGuardsPanic pins the guards that report a builtin arg its check should // have rejected. No data reaches them — checkFunc runs a builtin's check before // compileOps ever calls prep — so they are exercised directly. func TestArgGuardsPanic(t *testing.T) { for name, call := range map[string]func(){ "atoi on an unvalidated arg": func() { atoi("nope") }, "atof on an unvalidated arg": func() { atof("nope") }, } { mustPanic(t, name, call) } } // mustPanic fails unless call panics, which is what separates a reported // invariant break from a silently wrong value. func mustPanic(t *testing.T, name string, call func()) { t.Helper() defer func() { if recover() == nil { t.Errorf("%s: no panic, want the invariant reported", name) } }() call() }