package ulid import ( "sort" "strings" "testing" "time" ) func TestNewShape(t *testing.T) { id := New() if len(id) != Len { t.Fatalf("len = %d, want %d", len(id), Len) } for i := 0; i < len(id); i++ { if !strings.ContainsRune(alphabet, rune(id[i])) { t.Fatalf("char %q at %d not in Crockford alphabet", id[i], i) } } if !IsValid(id) { t.Fatalf("IsValid(%q) = false", id) } } func TestUniqueness(t *testing.T) { const n = 10000 seen := make(map[string]struct{}, n) for i := 0; i < n; i++ { id := New() if _, dup := seen[id]; dup { t.Fatalf("duplicate ULID %q after %d ids", id, i) } seen[id] = struct{}{} } } func TestLexicographicTimeOrdering(t *testing.T) { base := time.Now() var ids []string for i := 0; i < 50; i++ { ids = append(ids, At(base.Add(time.Duration(i)*time.Millisecond))) } if !sort.StringsAreSorted(ids) { t.Fatal("ULIDs with increasing timestamps are not lexicographically sorted") } } func TestTimestampRoundTrip(t *testing.T) { tests := []time.Time{ time.UnixMilli(0), time.Date(2026, 6, 12, 12, 0, 0, 0, time.UTC), time.Now(), } for _, want := range tests { id := At(want) got, err := Timestamp(id) if err != nil { t.Fatalf("Timestamp(%q): %v", id, err) } if got.UnixMilli() != want.UnixMilli() { t.Errorf("timestamp = %v, want %v", got.UnixMilli(), want.UnixMilli()) } } } func TestIsValid(t *testing.T) { tests := []struct { in string want bool }{ {New(), true}, {strings.ToLower(New()), true}, // Crockford decoding is case-insensitive {"", false}, {"short", false}, {strings.Repeat("0", 25), false}, {strings.Repeat("0", 27), false}, {strings.Repeat("U", 26), false}, // U not in alphabet {"8" + strings.Repeat("0", 25), false}, // timestamp overflow (>48 bits) {"7" + strings.Repeat("Z", 25), true}, } for _, tt := range tests { if got := IsValid(tt.in); got != tt.want { t.Errorf("IsValid(%q) = %v, want %v", tt.in, got, tt.want) } } } func TestTimestampRejectsInvalid(t *testing.T) { if _, err := Timestamp("definitely-not-a-ulid!!!!!"); err == nil { t.Fatal("want error") } }