diff --git a/go/apps/api/routes/v2_keys_create_key/200_test.go b/go/apps/api/routes/v2_keys_create_key/200_test.go index f847c359a0..5fc02a669f 100644 --- a/go/apps/api/routes/v2_keys_create_key/200_test.go +++ b/go/apps/api/routes/v2_keys_create_key/200_test.go @@ -6,7 +6,9 @@ import ( "net/http" "testing" + "github.com/oapi-codegen/nullable" "github.com/stretchr/testify/require" + "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_create_key" "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/ptr" @@ -289,3 +291,46 @@ func TestCreateKeyConcurrentWithSameExternalId(t *testing.T) { require.Equal(t, sharedIdentityID, identity.ID) require.Equal(t, externalID, identity.ExternalID) } + +func TestCreateKeyWithCreditsRemainingNull(t *testing.T) { + t.Parallel() + + h := testutil.NewHarness(t) + + route := &handler.Handler{ + Logger: h.Logger, + DB: h.DB, + Keys: h.Keys, + Auditlogs: h.Auditlogs, + Vault: h.Vault, + } + + h.Register(route) + + // Create API using testutil helper + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_key") + + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + } + + t.Run("credits.remaining null without refill should succeed", func(t *testing.T) { + req := handler.Request{ + ApiId: api.ID, + Credits: &openapi.KeyCreditsData{ + Remaining: nullable.NewNullNullable[int64](), + }, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, 200, res.Status) + require.NotNil(t, res.Body) + require.NotEmpty(t, res.Body.Data.KeyId) + require.NotEmpty(t, res.Body.Data.Key) + }) +} diff --git a/go/apps/api/routes/v2_keys_create_key/400_test.go b/go/apps/api/routes/v2_keys_create_key/400_test.go index 98fc8e791c..d636488116 100644 --- a/go/apps/api/routes/v2_keys_create_key/400_test.go +++ b/go/apps/api/routes/v2_keys_create_key/400_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/oapi-codegen/nullable" "github.com/stretchr/testify/require" "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_create_key" @@ -167,4 +168,22 @@ func TestCreateKeyBadRequest(t *testing.T) { require.Equal(t, 400, res.Status) require.NotNil(t, res.Body) }) + + t.Run("credits.remaining null with refill should error", func(t *testing.T) { + req := handler.Request{ + ApiId: api.ID, + Credits: &openapi.KeyCreditsData{ + Remaining: nullable.NewNullNullable[int64](), + Refill: &openapi.KeyCreditsRefill{ + Amount: 100, + Interval: openapi.KeyCreditsRefillIntervalDaily, + }, + }, + } + + res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) + require.Equal(t, 400, res.Status) + require.NotNil(t, res.Body) + require.Contains(t, res.Body.Error.Detail, "credits.remaining") + }) } diff --git a/go/apps/api/routes/v2_keys_create_key/handler.go b/go/apps/api/routes/v2_keys_create_key/handler.go index 2654e92b5b..9bbca58111 100644 --- a/go/apps/api/routes/v2_keys_create_key/handler.go +++ b/go/apps/api/routes/v2_keys_create_key/handler.go @@ -272,7 +272,18 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } if req.Credits != nil { - if req.Credits.Remaining.IsSpecified() { + // If refill is set, remaining must be specified and not null + if req.Credits.Refill != nil { + if !req.Credits.Remaining.IsSpecified() || req.Credits.Remaining.IsNull() { + return fault.New("missing credits.remaining", + fault.Code(codes.App.Validation.InvalidInput.URN()), + fault.Internal("credits.remaining required when refill is set"), + fault.Public("`credits.remaining` must be provided when `credits.refill` is set."), + ) + } + } + + if req.Credits.Remaining.IsSpecified() && !req.Credits.Remaining.IsNull() { insertKeyParams.RemainingRequests = sql.NullInt32{ Int32: int32(req.Credits.Remaining.MustGet()), // nolint:gosec Valid: true,