package nixcache
import (
"crypto/rand"
"crypto/rsa"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
func testKey(t *testing.T) *rsa.PrivateKey {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
return key
}
func parseClaims(t *testing.T, key *rsa.PrivateKey, token string) jwt.MapClaims {
t.Helper()
claims := jwt.MapClaims{}
_, err := jwt.ParseWithClaims(token, claims, func(*jwt.Token) (any, error) { return &key.PublicKey, nil },
jwt.WithValidMethods([]string{"RS256"}), jwt.WithExpirationRequired())
if err != nil {
t.Fatalf("parsing token: %v", err)
}
return claims
}
func TestMintTokenClaims(t *testing.T) {
key := testKey(t)
exp := time.Now().Add(time.Hour).Truncate(time.Second)
token, err := MintToken(key, "lease-1", "plc-abc", Grant{Pull: true, Push: true}, exp)
if err != nil {
t.Fatal(err)
}
claims := parseClaims(t, key, token)
if claims["sub"] != "lease-1" {
t.Errorf("sub = %v", claims["sub"])
}
if got := int64(claims["exp"].(float64)); got != exp.Unix() {
t.Errorf("exp = %d, want %d", got, exp.Unix())
}
if nbf, ok := claims["nbf"]; ok {
t.Errorf("nbf = %v, want none: attic checks it with no clock tolerance", nbf)
}
attic, ok := claims["https://jwt.attic.rs/v1"].(map[string]any)
if !ok {
t.Fatalf("no attic claim in %v", claims)
}
caches := attic["caches"].(map[string]any)
if len(caches) != 1 {
t.Fatalf("caches = %v, want exactly one", caches)
}
perms, ok := caches["plc-abc"].(map[string]any)
if !ok {
t.Fatalf("caches = %v, want plc-abc", caches)
}
want := map[string]any{"r": 1.0, "w": 1.0}
if len(perms) != len(want) || perms["r"] != want["r"] || perms["w"] != want["w"] {
t.Errorf("perms = %v, want %v", perms, want)
}
}
func TestMintTokenGrantFieldsMapToAtticBits(t *testing.T) {
key := testKey(t)
tests := []struct {
grant Grant
bit string
}{
{Grant{Pull: true}, "r"},
{Grant{Push: true}, "w"},
{Grant{Delete: true}, "d"},
{Grant{Create: true}, "cc"},
{Grant{ConfigureCache: true}, "cr"},
{Grant{ConfigureRetention: true}, "cq"},
}
for _, tt := range tests {
token, err := MintToken(key, "s", "c", tt.grant, time.Now().Add(time.Minute))
if err != nil {
t.Fatal(err)
}
attic := parseClaims(t, key, token)["https://jwt.attic.rs/v1"].(map[string]any)
perms := attic["caches"].(map[string]any)["c"].(map[string]any)
if len(perms) != 1 || perms[tt.bit] != 1.0 {
t.Errorf("grant %+v = %v, want only %q", tt.grant, perms, tt.bit)
}
}
}
func TestMintTokenRejectsPatternsAndBadInput(t *testing.T) {
key := testKey(t)
exp := time.Now().Add(time.Minute)
for _, cache := range []string{"*", "plc-*", "a?b", "", "-lead", strings.Repeat("a", 51), "a b"} {
if token, err := MintToken(key, "s", cache, Grant{Pull: true}, exp); err == nil {
t.Errorf("MintToken for cache %q = %q; want an error", cache, token)
}
}
if _, err := MintToken(key, "s", "c", Grant{Pull: true}, time.Now().Add(-time.Minute)); err == nil {
t.Error("an expired deadline minted a token")
}
if _, err := MintToken(key, "s", "c", Grant{}, exp); err == nil {
t.Error("an empty grant minted a token")
}
if _, err := MintToken(nil, "s", "c", Grant{Pull: true}, exp); err == nil {
t.Error("a nil key minted a token")
}
}