Files
zitadel/internal/api/grpc/application/v2/convert/convert_test.go
Livio Spring bd995b0b48 feat(api): move application service v2beta to GA (and deprecate v2beta) (#10846)
# Which Problems Are Solved

As part of our efforts to simplify the structure and versions of our
APIs, were moving all existing v2beta endpoints to v2 and deprecate
them. They will be removed in Zitadel V5.

# How the Problems Are Solved

- This PR moves app v2beta service and its endpoints to a corresponding
to application v2 version. The v2beta service and endpoints are
deprecated.
- The comments and have been improved and, where not already done, moved
from swagger annotations to proto.
- All required fields have been marked with (google.api.field_behavior)
= REQUIRED and validation rules have been added where missing.
- Name ID of the application always `application_id`, previously was
also `id` and `app_id`.
- Get rid of all `app` abbreviations and name it `application` including
the service name, `AppState` -> `ApplicationState` and `AppSorting` ->
`ApplicationSorting`
- Updated `CreateApplicationRequest`:
- renamed `creation_request_type` to `application_type` and all its
options to `XY_configuration` instead of `XY_request`
- `RegenerateClientSecret`
  - renamed method to `GenerateClientSecret`
  - removed `app_type` from request
- `ListApplicationRequest`:
  - removed required `project_id` and provided it as a filter
- Type `ApplicationNameQuery` has been renamed to
`ApplicationNameFilter` as its usage in the request
- Renamed all fields and types from `config` to `configuration`
- Updated `DeleteApplicationKeyRequest`
  - removed `organization_id`
- Updated `GetApplicationKeyRequest`:
  - removed `project_id`, `application_id` and `organization_id``
- Updated `ListApplicationKeysRequest`:
  - removed oneOf `resource_id` and moved the options into filters
- Name ID of the application key always `key_id`.
- removed unnecessary package prefixed (`zitadel.application.v2`)
- formatted using `buf`

# Additional Changes

None

# Additional Context

- part of https://github.com/zitadel/zitadel/issues/10772
- requires backport to v4.x

(cherry picked from commit 0281670030)
2025-10-27 08:56:27 +01:00

721 lines
19 KiB
Go

package convert
import (
"errors"
"fmt"
"net/url"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/application/v2"
filter_pb_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
filter_pb_v2_beta "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
)
func TestAppToPb(t *testing.T) {
t.Parallel()
now := time.Now()
tt := []struct {
testName string
inputQueryApp *query.App
expectedPbApp *application.Application
}{
{
testName: "full application conversion",
inputQueryApp: &query.App{
ID: "id",
CreationDate: now,
ChangeDate: now,
State: domain.AppStateActive,
Name: "test-application",
APIConfig: &query.APIApp{},
},
expectedPbApp: &application.Application{
ApplicationId: "id",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
State: application.ApplicationState_APPLICATION_STATE_ACTIVE,
Name: "test-application",
Configuration: &application.Application_ApiConfiguration{
ApiConfiguration: &application.APIConfiguration{},
},
},
},
{
testName: "nil application",
inputQueryApp: nil,
expectedPbApp: &application.Application{},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// When
res := AppToPb(tc.inputQueryApp)
// Then
assert.Equal(t, tc.expectedPbApp, res)
})
}
}
func TestListApplicationsRequestToModel(t *testing.T) {
t.Parallel()
validSearchByNameQuery, err := query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(filter_pb_v2_beta.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS), "test")
require.NoError(t, err)
validSearchByProjectQuery, err := query.NewAppProjectIDSearchQuery("project1")
require.NoError(t, err)
sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150}
tt := []struct {
testName string
req *application.ListApplicationsRequest
expectedResponse *query.AppSearchQueries
expectedError error
}{
{
testName: "invalid pagination limit",
req: &application.ListApplicationsRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true, Limit: uint32(sysDefaults.MaxQueryLimit + 1)},
},
expectedResponse: nil,
expectedError: zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", sysDefaults.MaxQueryLimit+1, sysDefaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded"),
},
{
testName: "empty request",
req: &application.ListApplicationsRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
Filters: []*application.ApplicationSearchFilter{},
},
expectedResponse: &query.AppSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AppColumnID,
},
Queries: []query.SearchQuery{},
},
},
{
testName: "valid request",
req: &application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{ProjectIdFilter: &application.ProjectIDFilter{ProjectId: "project1"}},
},
{
Filter: &application.ApplicationSearchFilter_NameFilter{NameFilter: &application.ApplicationNameFilter{Name: "test"}},
},
},
SortingColumn: application.ApplicationSorting_APPLICATION_SORT_BY_NAME,
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
},
expectedResponse: &query.AppSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AppColumnName,
},
Queries: []query.SearchQuery{
validSearchByProjectQuery,
validSearchByNameQuery,
},
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// When
got, err := ListApplicationsRequestToModel(sysDefaults, tc.req)
// Then
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedResponse, got)
})
}
}
func TestApplicationSortingToColumn(t *testing.T) {
t.Parallel()
tt := []struct {
name string
sorting application.ApplicationSorting
expected query.Column
}{
{
name: "sort by change date",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_CHANGE_DATE,
expected: query.AppColumnChangeDate,
},
{
name: "sort by creation date",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_CREATION_DATE,
expected: query.AppColumnCreationDate,
},
{
name: "sort by name",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_NAME,
expected: query.AppColumnName,
},
{
name: "sort by state",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_STATE,
expected: query.AppColumnState,
},
{
name: "sort by ID",
sorting: application.ApplicationSorting_APPLICATION_SORT_BY_ID,
expected: query.AppColumnID,
},
{
name: "unknown sorting defaults to ID",
sorting: application.ApplicationSorting(99),
expected: query.AppColumnID,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := appSortingToColumn(tc.sorting)
// Then
assert.Equal(t, tc.expected, result)
})
}
}
func TestAppStateToPb(t *testing.T) {
t.Parallel()
tt := []struct {
name string
state domain.AppState
expected application.ApplicationState
}{
{
name: "active state",
state: domain.AppStateActive,
expected: application.ApplicationState_APPLICATION_STATE_ACTIVE,
},
{
name: "inactive state",
state: domain.AppStateInactive,
expected: application.ApplicationState_APPLICATION_STATE_INACTIVE,
},
{
name: "removed state",
state: domain.AppStateRemoved,
expected: application.ApplicationState_APPLICATION_STATE_REMOVED,
},
{
name: "unspecified state",
state: domain.AppStateUnspecified,
expected: application.ApplicationState_APPLICATION_STATE_UNSPECIFIED,
},
{
name: "unknown state defaults to unspecified",
state: domain.AppState(99),
expected: application.ApplicationState_APPLICATION_STATE_UNSPECIFIED,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := appStateToPb(tc.state)
// Then
assert.Equal(t, tc.expected, result)
})
}
}
func TestAppConfigToPb(t *testing.T) {
t.Parallel()
tt := []struct {
name string
app *query.App
expected application.IsApplicationConfiguration
}{
{
name: "OIDC config",
app: &query.App{
OIDCConfig: &query.OIDCApp{},
},
expected: &application.Application_OidcConfiguration{
OidcConfiguration: &application.OIDCConfiguration{
ResponseTypes: []application.OIDCResponseType{},
GrantTypes: []application.OIDCGrantType{},
ComplianceProblems: []*application.OIDCLocalizedMessage{},
ClockSkew: &durationpb.Duration{},
},
},
},
{
name: "SAML config",
app: &query.App{
SAMLConfig: &query.SAMLApp{},
},
expected: &application.Application_SamlConfiguration{
SamlConfiguration: &application.SAMLConfiguration{},
},
},
{
name: "API config",
app: &query.App{
APIConfig: &query.APIApp{},
},
expected: &application.Application_ApiConfiguration{
ApiConfiguration: &application.APIConfiguration{},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := appConfigToPb(tc.app)
// Then
assert.Equal(t, tc.expected, result)
})
}
}
func TestLoginVersionToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
name string
version *application.LoginVersion
expectedVer *domain.LoginVersion
expectedURI *string
expectedError error
}{
{
name: "nil version",
version: nil,
expectedVer: gu.Ptr(domain.LoginVersionUnspecified),
expectedURI: gu.Ptr(""),
},
{
name: "login v1",
version: &application.LoginVersion{Version: &application.LoginVersion_LoginV1{LoginV1: &application.LoginV1{}}},
expectedVer: gu.Ptr(domain.LoginVersion1),
expectedURI: gu.Ptr(""),
},
{
name: "login v2 valid URI",
version: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.LoginV2{BaseUri: gu.Ptr("https://valid.url")}}},
expectedVer: gu.Ptr(domain.LoginVersion2),
expectedURI: gu.Ptr("https://valid.url"),
},
{
name: "login v2 invalid URI",
version: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.LoginV2{BaseUri: gu.Ptr("://invalid")}}},
expectedVer: gu.Ptr(domain.LoginVersion2),
expectedURI: gu.Ptr("://invalid"),
expectedError: &url.Error{Op: "parse", URL: "://invalid", Err: errors.New("missing protocol scheme")},
},
{
name: "unknown version type",
version: &application.LoginVersion{},
expectedVer: gu.Ptr(domain.LoginVersionUnspecified),
expectedURI: gu.Ptr(""),
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
version, uri, err := loginVersionToDomain(tc.version)
// Then
assert.Equal(t, tc.expectedVer, version)
assert.Equal(t, tc.expectedURI, uri)
assert.Equal(t, tc.expectedError, err)
})
}
}
func TestLoginVersionToPb(t *testing.T) {
t.Parallel()
tt := []struct {
name string
version domain.LoginVersion
baseURI *string
expected *application.LoginVersion
}{
{
name: "unspecified version",
version: domain.LoginVersionUnspecified,
baseURI: nil,
expected: nil,
},
{
name: "login v1",
version: domain.LoginVersion1,
baseURI: nil,
expected: &application.LoginVersion{
Version: &application.LoginVersion_LoginV1{
LoginV1: &application.LoginV1{},
},
},
},
{
name: "login v2",
version: domain.LoginVersion2,
baseURI: gu.Ptr("https://example.com"),
expected: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.LoginV2{
BaseUri: gu.Ptr("https://example.com"),
},
},
},
},
{
name: "unknown version",
version: domain.LoginVersion(99),
baseURI: nil,
expected: nil,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := loginVersionToPb(tc.version, tc.baseURI)
// Then
assert.Equal(t, tc.expected, result)
})
}
}
func TestAppQueryToModel(t *testing.T) {
t.Parallel()
validAppNameSearchQuery, err := query.NewAppNameSearchQuery(query.TextEquals, "test")
require.NoError(t, err)
validAppStateSearchQuery, err := query.NewAppStateSearchQuery(domain.AppStateActive)
require.NoError(t, err)
tt := []struct {
name string
query *application.ApplicationSearchFilter
expectedQuery query.SearchQuery
expectedError error
}{
{
name: "name query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_NameFilter{
NameFilter: &application.ApplicationNameFilter{
Name: "test",
Method: filter_pb_v2.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
expectedQuery: validAppNameSearchQuery,
},
{
name: "state query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_StateFilter{
StateFilter: application.ApplicationState_APPLICATION_STATE_ACTIVE,
},
},
expectedQuery: validAppStateSearchQuery,
},
{
name: "api application only query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_TypeFilter{
TypeFilter: application.ApplicationType_APPLICATION_TYPE_API,
},
},
expectedQuery: &query.NotNullQuery{
Column: query.AppAPIConfigColumnAppID,
},
},
{
name: "oidc application only query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_TypeFilter{
TypeFilter: application.ApplicationType_APPLICATION_TYPE_OIDC,
},
},
expectedQuery: &query.NotNullQuery{
Column: query.AppOIDCConfigColumnAppID,
},
},
{
name: "saml application only query",
query: &application.ApplicationSearchFilter{
Filter: &application.ApplicationSearchFilter_TypeFilter{
TypeFilter: application.ApplicationType_APPLICATION_TYPE_SAML,
},
},
expectedQuery: &query.NotNullQuery{
Column: query.AppSAMLConfigColumnAppID,
},
},
{
name: "invalid query type",
query: &application.ApplicationSearchFilter{},
expectedQuery: nil,
expectedError: zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid"),
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result, err := applicationFilterToQuery(tc.query)
// Then
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedQuery, result)
})
}
}
func TestListApplicationKeysRequestToDomain(t *testing.T) {
t.Parallel()
resourceOwnerQuery, err := query.NewAuthNKeyResourceOwnerQuery("org1")
require.NoError(t, err)
projectIDQuery, err := query.NewAuthNKeyAggregateIDQuery("project1")
require.NoError(t, err)
appIDQuery, err := query.NewAuthNKeyObjectIDQuery("app1")
require.NoError(t, err)
sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150}
tt := []struct {
name string
req *application.ListApplicationKeysRequest
expectedResult *query.AuthNKeySearchQueries
expectedError error
}{
{
name: "invalid pagination limit",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true, Limit: uint32(sysDefaults.MaxQueryLimit + 1)},
},
expectedResult: nil,
expectedError: zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", sysDefaults.MaxQueryLimit+1, sysDefaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded"),
},
{
name: "empty request",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
},
expectedResult: &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AuthNKeyColumnID,
},
Queries: []query.SearchQuery{},
},
},
{
name: "only organization id",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_OrganizationIdFilter{
OrganizationIdFilter: &application.ApplicationKeyOrganizationIDFilter{OrganizationId: "org1"},
}},
},
},
expectedResult: &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AuthNKeyColumnID,
},
Queries: []query.SearchQuery{
resourceOwnerQuery,
},
},
},
{
name: "only project id",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ApplicationKeyProjectIDFilter{ProjectId: "project1"},
}},
},
},
expectedResult: &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AuthNKeyColumnID,
},
Queries: []query.SearchQuery{
projectIDQuery,
},
},
},
{
name: "only application id",
req: &application.ListApplicationKeysRequest{
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
Filters: []*application.ApplicationKeySearchFilter{
{Filter: &application.ApplicationKeySearchFilter_ApplicationIdFilter{
ApplicationIdFilter: &application.ApplicationKeyApplicationIDFilter{ApplicationId: "app1"},
}},
},
},
expectedResult: &query.AuthNKeySearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AuthNKeyColumnID,
},
Queries: []query.SearchQuery{
appIDQuery,
},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result, err := ListApplicationKeysRequestToDomain(sysDefaults, tc.req)
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedResult, result)
})
}
}
func TestApplicationKeysToPb(t *testing.T) {
t.Parallel()
now := time.Now()
tt := []struct {
name string
input []*query.AuthNKey
expected []*application.ApplicationKey
}{
{
name: "multiple keys",
input: []*query.AuthNKey{
{
ID: "key1",
AggregateID: "project1",
ApplicationID: "app1",
CreationDate: now,
ResourceOwner: "org1",
Expiration: now.Add(24 * time.Hour),
Type: domain.AuthNKeyTypeJSON,
},
{
ID: "key2",
AggregateID: "project2",
ApplicationID: "app1",
CreationDate: now.Add(-time.Hour),
ResourceOwner: "org2",
Expiration: now.Add(48 * time.Hour),
Type: domain.AuthNKeyTypeNONE,
},
},
expected: []*application.ApplicationKey{
{
KeyId: "key1",
ApplicationId: "app1",
ProjectId: "project1",
CreationDate: timestamppb.New(now),
OrganizationId: "org1",
ExpirationDate: timestamppb.New(now.Add(24 * time.Hour)),
},
{
KeyId: "key2",
ApplicationId: "app1",
ProjectId: "project2",
CreationDate: timestamppb.New(now.Add(-time.Hour)),
OrganizationId: "org2",
ExpirationDate: timestamppb.New(now.Add(48 * time.Hour)),
},
},
},
{
name: "empty slice",
input: []*query.AuthNKey{},
expected: []*application.ApplicationKey{},
},
{
name: "nil input",
input: nil,
expected: []*application.ApplicationKey{},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := ApplicationKeysToPb(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}