Greenfield Go multi-tenant IPFS Pinning Service wire-compatible with the
IPFS Pinning Services API spec. Paired 1:1 with Kubo over localhost RPC,
clustered via embedded NATS JetStream, Postgres source-of-truth with
RLS-enforced tenancy, Fiber + huma v2 for the HTTP surface, Authentik
OIDC for session login with kid-rotated HS256 JWT API tokens.
Feature-complete against the 22-milestone build plan, including the
ship-it v1.0 gap items:
* admin CLIs: drain/uncordon, maintenance, mint-token, rotate-key,
prune-denylist, rebalance --dry-run, cache-stats, cluster-presences
* TTL leader election via NATS KV, fence tokens, JetStream dedup
* rebalancer (plan/apply split), reconciler, requeue sweeper
* ristretto caches with NATS-backed cross-node invalidation
(placements live-nodes + token denylist)
* maintenance watchdog for stuck cluster-pause flag
* Prometheus /metrics with CIDR ACL, HTTP/pin/scheduler/cache gauges
* rate limiting: session (10/min) + anonymous global (120/min)
* integration tests: rebalance, refcount multi-org, RLS belt
* goreleaser (tar + deb/rpm/apk + Alpine Docker) targeting Gitea
Stack: Cobra/Viper, Fiber v2 + huma v2, embedded NATS JetStream,
pgx/sqlc/golang-migrate, ristretto, TypeID, prometheus/client_golang,
testcontainers-go.
145 lines
3.8 KiB
Go
145 lines
3.8 KiB
Go
package ids_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"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, so later-generated IDs should sort after
|
|
// earlier ones lexicographically. This is the property that gives
|
|
// Postgres index locality on (org_id, id desc).
|
|
prev := ids.MustNewPin().String()
|
|
for i := 0; i < 32; i++ {
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|