Pin bouncer Scope serialization, party Activated order, lagoon migration, compile/overlap/mint/expiry/validation, guard/debounce/resolver, interactive-only, account token API and CLI on Postgres. Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
4.8 KiB
Go
159 lines
4.8 KiB
Go
package cabana
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
)
|
|
|
|
func TestAPITokenMint(t *testing.T) {
|
|
seen := map[string]bool{}
|
|
for i := 0; i < 1000; i++ {
|
|
plain, hash, hint, err := mintAPIToken()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !wellFormedAPIToken(plain) {
|
|
t.Fatalf("plain %q", plain)
|
|
}
|
|
if len(hash) != 64 {
|
|
t.Fatalf("hash len %d", len(hash))
|
|
}
|
|
for _, c := range hash {
|
|
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
|
|
t.Fatalf("hash non-hex %q", hash)
|
|
}
|
|
}
|
|
if hint != plain[len(plain)-4:] {
|
|
t.Fatalf("hint %q", hint)
|
|
}
|
|
if seen[plain] {
|
|
t.Fatalf("duplicate plain at %d", i)
|
|
}
|
|
seen[plain] = true
|
|
if hashAPIToken(plain) != hash {
|
|
t.Fatal("hashAPIToken mismatch")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseTokenExpiry(t *testing.T) {
|
|
now := time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
name string
|
|
expires string
|
|
cli bool
|
|
wantKey string
|
|
check func(*testing.T, *time.Time)
|
|
}{
|
|
{"30d", "30d", false, "", func(t *testing.T, got *time.Time) {
|
|
if got == nil || !got.Equal(now.Add(30*24*time.Hour)) {
|
|
t.Fatalf("%v", got)
|
|
}
|
|
}},
|
|
{"90d", "90d", false, "", func(t *testing.T, got *time.Time) {
|
|
if got == nil || !got.Equal(now.Add(90*24*time.Hour)) {
|
|
t.Fatalf("%v", got)
|
|
}
|
|
}},
|
|
{"365d", "365d", false, "", func(t *testing.T, got *time.Time) {
|
|
if got == nil || !got.Equal(now.Add(365*24*time.Hour)) {
|
|
t.Fatalf("%v", got)
|
|
}
|
|
}},
|
|
{"never", "never", false, "", func(t *testing.T, got *time.Time) {
|
|
if got != nil {
|
|
t.Fatalf("%v", got)
|
|
}
|
|
}},
|
|
{"absent defaults 90d", "", false, "", func(t *testing.T, got *time.Time) {
|
|
if got == nil || !got.Equal(now.Add(90*24*time.Hour)) {
|
|
t.Fatalf("%v", got)
|
|
}
|
|
}},
|
|
{"cli future date", "2026-07-01", true, "", func(t *testing.T, got *time.Time) {
|
|
want := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
|
|
if got == nil || !got.Equal(want) {
|
|
t.Fatalf("%v", got)
|
|
}
|
|
}},
|
|
{"cli today refused", "2026-06-15", true, "api_tokens.expires_invalid", nil},
|
|
{"cli past refused", "2026-01-01", true, "api_tokens.expires_invalid", nil},
|
|
{"garbage api", "tomorrow", false, "api_tokens.expires_invalid", nil},
|
|
{"date in api mode refused", "2026-07-01", false, "api_tokens.expires_invalid", nil},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, key := parseTokenExpiry(tc.expires, now, tc.cli)
|
|
if key != tc.wantKey {
|
|
t.Fatalf("key = %q, want %q", key, tc.wantKey)
|
|
}
|
|
if tc.check != nil {
|
|
tc.check(t, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateTokenRequest(t *testing.T) {
|
|
reg, err := compileTokenCatalog([]party.Plugin{scopePlugin{id: "acme.demo", scopes: []pact.TokenScope{{
|
|
Name: "demo", Label: "Demo", Description: "D",
|
|
Permissions: []string{"acme.demo.*"},
|
|
Methods: []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"},
|
|
}}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
owner := &bouncer.Principal{
|
|
ID: 1, Backend: true,
|
|
PermissionGrants: map[string]bool{"acme.demo.access": true},
|
|
}
|
|
now := time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC)
|
|
|
|
_, details := validateTokenRequest(reg, owner, "", "90d", []string{"demo"}, now, false)
|
|
if details["name"] == nil {
|
|
t.Fatal("blank name")
|
|
}
|
|
long := strings.Repeat("x", 101)
|
|
if utf8.RuneCountInString(long) != 101 {
|
|
t.Fatal("fixture")
|
|
}
|
|
_, details = validateTokenRequest(reg, owner, long, "90d", []string{"demo"}, now, false)
|
|
if details["name"] == nil {
|
|
t.Fatal("over-100 name")
|
|
}
|
|
_, details = validateTokenRequest(reg, owner, "ok", "nope", []string{"demo"}, now, false)
|
|
if details["expires"] == nil {
|
|
t.Fatal("invalid expiry")
|
|
}
|
|
_, details = validateTokenRequest(reg, owner, "ok", "90d", nil, now, false)
|
|
if details["scopes"] == nil {
|
|
t.Fatal("empty scopes")
|
|
}
|
|
_, details = validateTokenRequest(reg, owner, "ok", "90d", []string{"missing"}, now, false)
|
|
if details["scopes"] == nil {
|
|
t.Fatal("unknown scope")
|
|
}
|
|
noOverlap := &bouncer.Principal{ID: 2, Backend: true, PermissionGrants: map[string]bool{"acme.other.x": true}}
|
|
_, details = validateTokenRequest(reg, noOverlap, "ok", "90d", []string{"demo"}, now, false)
|
|
if details["scopes"] == nil {
|
|
t.Fatal("non-overlapping scope")
|
|
}
|
|
cleaned, details := validateTokenRequest(reg, owner, "ok", "90d", []string{"demo", "*", "demo"}, now, false)
|
|
if details != nil {
|
|
t.Fatalf("star normalize details=%v", details)
|
|
}
|
|
if len(cleaned.Scopes) != 1 || cleaned.Scopes[0] != "*" {
|
|
t.Fatalf("star normalize = %#v", cleaned.Scopes)
|
|
}
|
|
cleaned, details = validateTokenRequest(reg, owner, "ok", "90d", []string{"demo", "demo"}, now, false)
|
|
if details != nil || len(cleaned.Scopes) != 1 || cleaned.Scopes[0] != "demo" {
|
|
t.Fatalf("dup removal = %#v details=%v", cleaned.Scopes, details)
|
|
}
|
|
}
|