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