Files
summercms/modules/cabana/api_tokens_test.go
Jakub Zych 78124ab86f test(16.3-04): full Go coverage for tokens, guard, account API and CLI
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>
2026-10-11 13:32:11 +02:00

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)
}
}