- ids: TestIDsAreTimeOrdered asserted strict lexicographic ordering of back-to-back UUIDv7s, but the sub-ms tail is random and not required to be monotonic. Sleep between samples so each ID lands in a distinct millisecond — the property that actually gives Postgres index locality on (org_id, id desc). - go.mod/go.sum: run go mod tidy. keyfunc/v3, prometheus/client_golang and testcontainers-go/modules/postgres are imported directly and should not be marked // indirect; also drops stale sum entries. - gofmt -w across 12 files flagged by the lint job. - security.yml: pin govulncheck to v1.2.0. @latest triggers a proxy lookup every run, which is the step that hung for 16m on the Gitea runner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
4.0 KiB
Go
150 lines
4.0 KiB
Go
package ids_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"anchorage/internal/pkg/ids"
|
|
)
|
|
|
|
func TestNewConstructorsReturnCorrectPrefixes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
gen func(t *testing.T) (string, string)
|
|
prefix string
|
|
}{
|
|
{"org", func(t *testing.T) (string, string) {
|
|
id, err := ids.NewOrg()
|
|
if err != nil {
|
|
t.Fatalf("NewOrg: %v", err)
|
|
}
|
|
return id.Prefix(), id.String()
|
|
}, ids.OrgPrefixToken},
|
|
{"user", func(t *testing.T) (string, string) {
|
|
id, err := ids.NewUser()
|
|
if err != nil {
|
|
t.Fatalf("NewUser: %v", err)
|
|
}
|
|
return id.Prefix(), id.String()
|
|
}, ids.UserPrefixToken},
|
|
{"pin", func(t *testing.T) (string, string) {
|
|
id, err := ids.NewPin()
|
|
if err != nil {
|
|
t.Fatalf("NewPin: %v", err)
|
|
}
|
|
return id.Prefix(), id.String()
|
|
}, ids.PinPrefixToken},
|
|
{"token", func(t *testing.T) (string, string) {
|
|
id, err := ids.NewToken()
|
|
if err != nil {
|
|
t.Fatalf("NewToken: %v", err)
|
|
}
|
|
return id.Prefix(), id.String()
|
|
}, ids.TokenPrefixToken},
|
|
{"node", func(t *testing.T) (string, string) {
|
|
id, err := ids.NewNode()
|
|
if err != nil {
|
|
t.Fatalf("NewNode: %v", err)
|
|
}
|
|
return id.Prefix(), id.String()
|
|
}, ids.NodePrefixToken},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
prefix, str := tt.gen(t)
|
|
if prefix != tt.prefix {
|
|
t.Errorf("Prefix() = %q, want %q", prefix, tt.prefix)
|
|
}
|
|
if !strings.HasPrefix(str, tt.prefix+"_") {
|
|
t.Errorf("String() = %q, want prefix %q", str, tt.prefix+"_")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMustConstructorsDoNotPanicInHappyPath(t *testing.T) {
|
|
// Smoke-test: they must return the same prefix as the error-returning
|
|
// counterpart, and not panic under normal operation.
|
|
if p := ids.MustNewOrg().Prefix(); p != ids.OrgPrefixToken {
|
|
t.Errorf("MustNewOrg prefix = %q", p)
|
|
}
|
|
if p := ids.MustNewUser().Prefix(); p != ids.UserPrefixToken {
|
|
t.Errorf("MustNewUser prefix = %q", p)
|
|
}
|
|
if p := ids.MustNewPin().Prefix(); p != ids.PinPrefixToken {
|
|
t.Errorf("MustNewPin prefix = %q", p)
|
|
}
|
|
if p := ids.MustNewToken().Prefix(); p != ids.TokenPrefixToken {
|
|
t.Errorf("MustNewToken prefix = %q", p)
|
|
}
|
|
if p := ids.MustNewNode().Prefix(); p != ids.NodePrefixToken {
|
|
t.Errorf("MustNewNode prefix = %q", p)
|
|
}
|
|
}
|
|
|
|
func TestIDsAreUniquePerCall(t *testing.T) {
|
|
// UUIDv7 suffixes must differ between calls even back-to-back.
|
|
a := ids.MustNewPin().String()
|
|
b := ids.MustNewPin().String()
|
|
if a == b {
|
|
t.Fatalf("two consecutive NewPin() calls produced the same id: %s", a)
|
|
}
|
|
}
|
|
|
|
func TestIDsAreTimeOrdered(t *testing.T) {
|
|
// UUIDv7 is time-ordered at millisecond granularity; the sub-ms
|
|
// tail is random and not required to be monotonic, so back-to-back
|
|
// calls in the same millisecond can sort in either direction. The
|
|
// property we actually rely on for Postgres index locality on
|
|
// (org_id, id desc) is that IDs separated by >=1ms sort in
|
|
// generation order — so sleep between samples.
|
|
prev := ids.MustNewPin().String()
|
|
for i := 0; i < 8; i++ {
|
|
time.Sleep(2 * time.Millisecond)
|
|
next := ids.MustNewPin().String()
|
|
if next <= prev {
|
|
t.Fatalf("UUIDv7 ordering violated: %q followed %q", next, prev)
|
|
}
|
|
prev = next
|
|
}
|
|
}
|
|
|
|
func TestParseRoundTrip(t *testing.T) {
|
|
orig := ids.MustNewOrg()
|
|
parsed, err := ids.ParseOrg(orig.String())
|
|
if err != nil {
|
|
t.Fatalf("ParseOrg: %v", err)
|
|
}
|
|
if parsed.String() != orig.String() {
|
|
t.Errorf("round-trip changed value: %q -> %q", orig, parsed)
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsWrongPrefix(t *testing.T) {
|
|
// An org ID string must not parse as a pin ID.
|
|
orgStr := ids.MustNewOrg().String()
|
|
if _, err := ids.ParsePin(orgStr); err == nil {
|
|
t.Errorf("ParsePin accepted an org id %q", orgStr)
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsGarbage(t *testing.T) {
|
|
tests := []string{
|
|
"",
|
|
"not-a-typeid",
|
|
"org_",
|
|
"_01h7rfxv6qefr4twn2jg5p4k3z",
|
|
"org_too-short",
|
|
"org_01h7rfxv6qefr4twn2jg5p4k3Z", // uppercase base32 char: invalid
|
|
}
|
|
for _, s := range tests {
|
|
t.Run(s, func(t *testing.T) {
|
|
if _, err := ids.ParseOrg(s); err == nil {
|
|
t.Errorf("ParseOrg(%q) should have failed", s)
|
|
}
|
|
})
|
|
}
|
|
}
|