Files
zitadel/internal/api/grpc/instance/v2/integration_test/instance_test.go
T
b339a3e696 feat: instance domain APIs with relational tables (#11051)
# Which Problems Are Solved

This PR implements the endpoints listed in
https://github.com/zitadel/zitadel/issues/10444 .

# How the Problems Are Solved

The implementation follows the same pattern as the Instance one. The
`Add` and `Remove` are implementing the `Commander` interface, while
`List` Domains endpoints are implementing the `Querier` interace.

The PR adds also some forgotten calls to the relational logic for the
Instance API. The commits are identifiable by the label `instance:`
prepended to them. This is the complete list: bafce01b0 , 6fe7257ab ,
e27bc9c34 , 8133ede73 , fde667953 , a3e1135ed , ee1a6f9c1

# Additional Context

- Closes #10444

---------

Co-authored-by: Elio Bischof <[email protected]>
Co-authored-by: Lebogang Phoshoko <[email protected]>
Co-authored-by: Wim Van Laer <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
Co-authored-by: Gayathri Vijayan <[email protected]>
2025-12-17 12:01:09 +01:00

331 lines
13 KiB
Go

//go:build integration
package instance_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
"github.com/zitadel/zitadel/pkg/grpc/instance/v2"
)
func TestDeleteInstance(t *testing.T) {
t.Parallel()
// Given
ctx, cancel := context.WithTimeout(t.Context(), 15*time.Minute)
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
// Event store instances
instES := integration.NewInstance(ctxWithSysAuthZ)
instanceESOwnerCtx := instES.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner)
instES2 := integration.NewInstance(ctxWithSysAuthZ)
instRelational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, instRelational, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
instanceRelationalOwnerCtx := instRelational.WithAuthorizationToken(t.Context(), integration.UserTypeIAMOwner)
instRelational2 := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, instRelational2, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
type instAndCtx struct {
testType string
i1 *integration.Instance
i2 *integration.Instance
ctx context.Context
}
testedInstancesAndCtxs := []instAndCtx{
{testType: "eventstore", i1: instES, i2: instES2, ctx: instanceESOwnerCtx},
{testType: "relational", i1: instRelational, i2: instRelational2, ctx: instanceRelationalOwnerCtx},
}
t.Cleanup(func() {
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
instES2.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES2.ID()})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
instRelational2.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational2.ID()})
})
for _, instanceWithCtx := range testedInstancesAndCtxs {
tt := []struct {
testName string
inputRequest *instance.DeleteInstanceRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedInstanceID string
expectsNullTimestamp bool
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID(),
},
inputContext: t.Context(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when invalid context and invalid id should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID() + "invalid",
},
inputContext: t.Context(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when instance token for invalid instance should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID() + "invalid",
},
inputContext: instanceWithCtx.ctx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when instance token for other instance should return unauthN error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i2.ID(),
},
inputContext: instanceWithCtx.ctx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when instance token for own instance should return unauthZ error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID(),
},
inputContext: instanceWithCtx.ctx,
expectedErrorCode: codes.PermissionDenied,
expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)",
},
{
// TODO(IAM-Marco): This test won't reach the relational side due to an interceptor
// automatically changing the instance in context to an empty one.
// The interceptor will look for the instance matching the instance ID passed here.
// Since the instance doesn't exist, it will put an empty instance in context.
// But an empty instance has no feature flags enabled.
testName: "when invalid id should return no error",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID() + "invalid",
},
inputContext: ctxWithSysAuthZ,
expectsNullTimestamp: true,
},
{
testName: "when delete succeeds should return deletion date",
inputRequest: &instance.DeleteInstanceRequest{
InstanceId: instanceWithCtx.i1.ID(),
},
inputContext: ctxWithSysAuthZ,
expectedInstanceID: instanceWithCtx.i1.ID(),
},
}
for _, tc := range tt {
t.Run(fmt.Sprintf("%s - %s", instanceWithCtx.testType, tc.testName), func(t *testing.T) {
// Test
res, err := instanceWithCtx.i1.Client.InstanceV2.DeleteInstance(tc.inputContext, tc.inputRequest)
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
if tc.expectedErrorMsg == "" {
require.NotNil(t, res)
require.Equal(t, tc.expectsNullTimestamp, res.GetDeletionDate() == nil)
}
})
}
}
}
func TestUpdateInstance(t *testing.T) {
t.Parallel()
// Given
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx)
instES := integration.NewInstance(ctxWithSysAuthZ)
instanceOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
orgOwnerCtxES := instES.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
inst2ES := integration.NewInstance(ctxWithSysAuthZ)
// Relational
instRelational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, instRelational, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
instanceOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner)
orgOwnerCtxRelational := instRelational.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner)
inst2Relational := integration.NewInstance(ctxWithSysAuthZ)
integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, inst2Relational, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) {
assert.True(tCollect, got.EnableRelationalTables.GetEnabled())
})
t.Cleanup(func() {
instES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instES.ID()})
inst2ES.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst2ES.ID()})
instRelational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: instRelational.ID()})
inst2Relational.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst2Relational.ID()})
})
type instAndCtx struct {
testType string
inst *integration.Instance
inst2 *integration.Instance
instanceOwnerCtx context.Context
orgOwnerCtx context.Context
}
testedInstancesAndCtxs := []instAndCtx{
{testType: "eventstore", inst: instES, inst2: inst2ES, instanceOwnerCtx: instanceOwnerCtxES, orgOwnerCtx: orgOwnerCtxES},
{testType: "relational", inst: instRelational, inst2: inst2Relational, instanceOwnerCtx: instanceOwnerCtxRelational, orgOwnerCtx: orgOwnerCtxRelational},
}
for _, instancesWithCtx := range testedInstancesAndCtxs {
tt := []struct {
testName string
inputRequest *instance.UpdateInstanceRequest
inputContext context.Context
expectedErrorMsg string
expectedErrorCode codes.Code
expectedNewName string
}{
{
testName: "when invalid context should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID(),
InstanceName: " ",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when invalid context and invalid id should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID() + "invalid",
InstanceName: " ",
},
inputContext: context.Background(),
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "auth header missing",
},
{
testName: "when instance token for invalid instance should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID() + "invalid",
InstanceName: " ",
},
inputContext: instancesWithCtx.instanceOwnerCtx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
testName: "when instance token for other instance should return unauthN error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst2.ID(),
InstanceName: " ",
},
inputContext: instancesWithCtx.instanceOwnerCtx,
expectedErrorCode: codes.Unauthenticated,
expectedErrorMsg: "Errors.Token.Invalid (AUTH-7fs1e)",
},
{
// TODO(IAM-Marco): Fix this test for relational case when permission checks are in place (see https://github.com/zitadel/zitadel/issues/10917)
testName: "when unauthZ context should return unauthZ error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID(),
InstanceName: "a valid name",
},
inputContext: instancesWithCtx.orgOwnerCtx,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "membership not found (AUTHZ-cdgFk)",
},
{
// TODO(IAM-Marco): This test won't reach the relational side due to an interceptor
// automatically changing the instance in context to an empty one.
// The interceptor will look for the instance matching the instance ID passed here.
// Since the instance doesn't exist, it will put an empty instance in context.
// But an empty instance has no feature flags enabled.
testName: "when invalid id should return not found error",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID() + "invalid",
InstanceName: "an-updated-name",
},
inputContext: ctxWithSysAuthZ,
expectedErrorCode: codes.NotFound,
expectedErrorMsg: "Errors.Instance.NotFound (INST-nuso2m)",
},
{
testName: "when update succeeds (instance owner) should change instance name",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst.ID(),
InstanceName: "an-updated-name",
},
inputContext: instancesWithCtx.instanceOwnerCtx,
expectedNewName: "an-updated-name",
},
{
testName: "when update succeeds should change instance name",
inputRequest: &instance.UpdateInstanceRequest{
InstanceId: instancesWithCtx.inst2.ID(),
InstanceName: "an-updated-name2",
},
inputContext: ctxWithSysAuthZ,
expectedNewName: "an-updated-name2",
},
}
for _, tc := range tt {
// TODO(IAM-Marco): Fix this test for relational case when permission checks are in place (see https://github.com/zitadel/zitadel/issues/10917)
if tc.testName == "when unauthZ context should return unauthZ error" && instancesWithCtx.testType == "relational" {
continue
}
t.Run(fmt.Sprintf("%s - %s", instancesWithCtx.testType, tc.testName), func(t *testing.T) {
// Test
res, err := instES.Client.InstanceV2.UpdateInstance(tc.inputContext, tc.inputRequest)
// Verify
assert.Equal(t, tc.expectedErrorCode, status.Code(err))
assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message())
if tc.expectedErrorMsg == "" {
require.NotNil(t, res)
assert.NotEmpty(t, res.GetChangeDate())
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
retrievedInstance, err := instES.Client.InstanceV2.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: tc.inputRequest.GetInstanceId()})
require.Nil(tt, err)
assert.Equal(tt, tc.expectedNewName, retrievedInstance.GetInstance().GetName())
}, retryDuration, tick, "timeout waiting for expected execution result")
}
})
}
}
}