Files
2026-08-10 18:24:37 +02:00

227 lines
6.9 KiB
Go

package lure
import (
"strings"
"testing"
)
func TestIsCandidate(t *testing.T) {
cases := []struct {
in string
want bool
}{
{"4H7K9QM2XR3T", true},
{"zQ8fRt2mKp9x", true},
// an operator written code may be any text, so no alphabet is applied
{"special-42", true},
{"Special_42", true},
{"hello", true},
// a single dot is allowed so a lure can end in invoice.pdf
{"invoice.pdf", true},
{"main.4f3a2b1c.js", true},
// values that could not sit in a single path segment unescaped
{"hello world", false},
{"a/b", false},
{"a%2Fb", false},
{"a?b", false},
{"a#b", false},
{"", false},
// a browser resolves a lone dot away before sending, and the resolver
// refuses a whole path carrying a doubled dot rather than cleaning it
{".", false},
{"..", false},
{"...", false},
{"invoice..pdf", false},
{"v1..2", false},
// a single dot elsewhere in the segment is still fine
{".hidden", true},
{strings.Repeat("a", MaxCustomLength), true},
{strings.Repeat("a", MaxCustomLength+1), false},
}
for _, c := range cases {
if got := IsCandidate(c.in); got != c.want {
t.Errorf("IsCandidate(%q) = %v, want %v", c.in, got, c.want)
}
}
}
func TestIsCandidateRejectsUnescapableValues(t *testing.T) {
for _, in := range []string{"a/b", "%2Fetc", "a\\b", "a b"} {
if IsCandidate(in) {
t.Errorf("IsCandidate(%q) should be false", in)
}
}
}
func TestNewCustomCodeIsKeptVerbatim(t *testing.T) {
// an operator written code reaches the URL as chosen, so Special-42
// and special-42 stay distinct links
for _, in := range []string{"special-42", "Special_42", "HR-Survey-2026", "invoice.pdf"} {
if got := NewCustomCode(in).Display; got != in {
t.Errorf("display form was altered: %q -> %q", in, got)
}
}
}
func TestGenerateLength(t *testing.T) {
for _, algorithm := range []Algorithm{AlgorithmCrockford32, AlgorithmBase58} {
for length := MinLength; length <= MaxLength; length++ {
code, err := Generate(algorithm, length)
if err != nil {
t.Fatalf("Generate(%s, %d) failed: %v", algorithm, length, err)
}
if len(code.Display) != length {
t.Errorf("Generate(%s, %d) gave length %d", algorithm, length, len(code.Display))
}
}
}
}
func TestGenerateStaysInsideItsAlphabet(t *testing.T) {
for _, algorithm := range []Algorithm{AlgorithmCrockford32, AlgorithmBase58} {
alphabet := AlphabetFor(algorithm)
for i := 0; i < 500; i++ {
code, err := Generate(algorithm, MaxLength)
if err != nil {
t.Fatalf("Generate failed: %v", err)
}
for _, r := range code.Display {
if !strings.ContainsRune(alphabet, r) {
t.Fatalf("%s produced %q outside its alphabet: %q", algorithm, string(r), code.Display)
}
}
}
}
}
func TestGenerateCoversEachAlphabet(t *testing.T) {
// rejection sampling must reach every symbol and accept enough draws to
// terminate. an alphabet dividing 256 gives a limit of 256, which held in a
// byte would be zero and reject everything.
for _, algorithm := range []Algorithm{AlgorithmCrockford32, AlgorithmBase58} {
alphabet := AlphabetFor(algorithm)
seen := map[rune]bool{}
for i := 0; i < 8000; i++ {
code, err := Generate(algorithm, MaxLength)
if err != nil {
t.Fatalf("Generate failed: %v", err)
}
for _, r := range code.Display {
seen[r] = true
}
}
for _, r := range alphabet {
if !seen[r] {
t.Errorf("%s never generated symbol %q", algorithm, string(r))
}
}
}
}
func TestBase58UsesBothCases(t *testing.T) {
// base58 treats the two cases as distinct symbols, which is why matching is
// never folded
lower := false
upper := false
for i := 0; i < 500 && (!lower || !upper); i++ {
code, err := Generate(AlgorithmBase58, MaxLength)
if err != nil {
t.Fatalf("Generate failed: %v", err)
}
if strings.ToLower(code.Display) != code.Display {
upper = true
}
if strings.ToUpper(code.Display) != code.Display {
lower = true
}
}
if !lower || !upper {
t.Error("base58 should produce both cases")
}
}
func TestGenerateExcludesConfusableGlyphs(t *testing.T) {
for i := 0; i < 500; i++ {
crockford, err := Generate(AlgorithmCrockford32, MaxLength)
if err != nil {
t.Fatalf("Generate failed: %v", err)
}
if strings.ContainsAny(crockford.Display, "ILOU") {
t.Fatalf("crockford code contains an excluded glyph: %q", crockford.Display)
}
base58, err := Generate(AlgorithmBase58, MaxLength)
if err != nil {
t.Fatalf("Generate failed: %v", err)
}
if strings.ContainsAny(base58.Display, "0OIl") {
t.Fatalf("base58 code contains an excluded glyph: %q", base58.Display)
}
}
}
func TestGenerateRejectsBadInput(t *testing.T) {
if _, err := Generate("nope", DefaultLength); err == nil {
t.Error("expected an error for an unknown algorithm")
}
if _, err := Generate(AlgorithmCrockford32, MinLength-1); err == nil {
t.Error("expected an error for a length below the minimum")
}
if _, err := Generate(AlgorithmCrockford32, MaxLength+1); err == nil {
t.Error("expected an error for a length above the maximum")
}
}
func TestGenerateBatch(t *testing.T) {
// a schedule run draws every code at once, so the batch must return exactly
// what was asked for and stay in its alphabet across entropy block refills
for _, algorithm := range []Algorithm{AlgorithmCrockford32, AlgorithmBase58} {
alphabet := AlphabetFor(algorithm)
codes, err := GenerateBatch(algorithm, DefaultLength, 5000)
if err != nil {
t.Fatalf("GenerateBatch(%s) failed: %v", algorithm, err)
}
if len(codes) != 5000 {
t.Fatalf("GenerateBatch(%s) gave %d codes, want 5000", algorithm, len(codes))
}
seen := map[string]bool{}
for _, code := range codes {
if len(code.Display) != DefaultLength {
t.Fatalf("code %q is not %d characters", code.Display, DefaultLength)
}
for _, r := range code.Display {
if !strings.ContainsRune(alphabet, r) {
t.Fatalf("%s produced %q outside its alphabet: %q", algorithm, string(r), code.Display)
}
}
seen[code.Display] = true
}
// the allocator dedupes, so a batch need not be distinct, but a broken draw
// repeating one code shows up here
if len(seen) < 4990 {
t.Errorf("%s produced only %d distinct codes out of 5000", algorithm, len(seen))
}
}
}
func TestGenerateBatchRejectsBadInput(t *testing.T) {
if _, err := GenerateBatch("nope", DefaultLength, 1); err == nil {
t.Error("expected an error for an unknown algorithm")
}
if _, err := GenerateBatch(AlgorithmCrockford32, MinLength-1, 1); err == nil {
t.Error("expected an error for a length below the minimum")
}
codes, err := GenerateBatch(AlgorithmCrockford32, DefaultLength, 0)
if err != nil || len(codes) != 0 {
t.Errorf("GenerateBatch with n 0 = %v,%v want empty,nil", codes, err)
}
}
func TestIsValidAlgorithm(t *testing.T) {
if !IsValidAlgorithm(AlgorithmCrockford32) || !IsValidAlgorithm(AlgorithmBase58) {
t.Error("both shipped algorithms should be valid")
}
if IsValidAlgorithm("base64") {
t.Error("unknown algorithm should not validate")
}
}