feat: App API v2 (#10077)

# Which Problems Are Solved

This PR *partially* addresses #9450 . Specifically, it implements the
resource based API for the apps. APIs for app keys ARE not part of this
PR.

# How the Problems Are Solved

- `CreateApplication`, `PatchApplication` (update) and
`RegenerateClientSecret` endpoints are now unique for all app types:
API, SAML and OIDC apps.
  - All new endpoints have integration tests
  - All new endpoints are using permission checks V2

# Additional Changes

- The `ListApplications` endpoint allows to do sorting (see protobuf for
details) and filtering by app type (see protobuf).
- SAML and OIDC update endpoint can now receive requests for partial
updates

# Additional Context

Partially addresses #9450
This commit is contained in:
Marco A.
2025-06-27 17:25:44 +02:00
committed by GitHub
parent 016676e1dc
commit 2691dae2b6
48 changed files with 6845 additions and 603 deletions

View File

@@ -0,0 +1,60 @@
package convert
import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
func CreateAPIApplicationRequestToDomain(name, projectID, appID string, app *app.CreateAPIApplicationRequest) *domain.APIApp {
return &domain.APIApp{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
},
AppName: name,
AppID: appID,
AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()),
}
}
func UpdateAPIApplicationConfigurationRequestToDomain(appID, projectID string, app *app.UpdateAPIApplicationConfigurationRequest) *domain.APIApp {
return &domain.APIApp{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
},
AppID: appID,
AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()),
}
}
func appAPIConfigToPb(apiApp *query.APIApp) app.ApplicationConfig {
return &app.Application_ApiConfig{
ApiConfig: &app.APIConfig{
ClientId: apiApp.ClientID,
AuthMethodType: apiAuthMethodTypeToPb(apiApp.AuthMethodType),
},
}
}
func apiAuthMethodTypeToDomain(authType app.APIAuthMethodType) domain.APIAuthMethodType {
switch authType {
case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC:
return domain.APIAuthMethodTypeBasic
case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT:
return domain.APIAuthMethodTypePrivateKeyJWT
default:
return domain.APIAuthMethodTypeBasic
}
}
func apiAuthMethodTypeToPb(methodType domain.APIAuthMethodType) app.APIAuthMethodType {
switch methodType {
case domain.APIAuthMethodTypeBasic:
return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC
case domain.APIAuthMethodTypePrivateKeyJWT:
return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
default:
return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC
}
}

View File

@@ -0,0 +1,149 @@
package convert
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
func TestCreateAPIApplicationRequestToDomain(t *testing.T) {
t.Parallel()
tests := []struct {
name string
appName string
projectID string
appID string
req *app.CreateAPIApplicationRequest
want *domain.APIApp
}{
{
name: "basic auth method",
appName: "my-app",
projectID: "proj-1",
appID: "someID",
req: &app.CreateAPIApplicationRequest{
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
},
want: &domain.APIApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppName: "my-app",
AuthMethodType: domain.APIAuthMethodTypeBasic,
AppID: "someID",
},
},
{
name: "private key jwt",
appName: "jwt-app",
projectID: "proj-2",
req: &app.CreateAPIApplicationRequest{
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
},
want: &domain.APIApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"},
AppName: "jwt-app",
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// When
got := CreateAPIApplicationRequestToDomain(tt.appName, tt.projectID, tt.appID, tt.req)
// Then
assert.Equal(t, tt.want, got)
})
}
}
func TestUpdateAPIApplicationConfigurationRequestToDomain(t *testing.T) {
t.Parallel()
tests := []struct {
name string
appID string
projectID string
req *app.UpdateAPIApplicationConfigurationRequest
want *domain.APIApp
}{
{
name: "basic auth method",
appID: "app-1",
projectID: "proj-1",
req: &app.UpdateAPIApplicationConfigurationRequest{
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
},
want: &domain.APIApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppID: "app-1",
AuthMethodType: domain.APIAuthMethodTypeBasic,
},
},
{
name: "private key jwt",
appID: "app-2",
projectID: "proj-2",
req: &app.UpdateAPIApplicationConfigurationRequest{
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
},
want: &domain.APIApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"},
AppID: "app-2",
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// When
got := UpdateAPIApplicationConfigurationRequestToDomain(tt.appID, tt.projectID, tt.req)
// Then
assert.Equal(t, tt.want, got)
})
}
}
func Test_apiAuthMethodTypeToPb(t *testing.T) {
t.Parallel()
tt := []struct {
name string
methodType domain.APIAuthMethodType
expectedResult app.APIAuthMethodType
}{
{
name: "basic auth method",
methodType: domain.APIAuthMethodTypeBasic,
expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
},
{
name: "private key jwt",
methodType: domain.APIAuthMethodTypePrivateKeyJWT,
expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
},
{
name: "unknown auth method defaults to basic",
expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
res := apiAuthMethodTypeToPb(tc.methodType)
// Then
assert.Equal(t, tc.expectedResult, res)
})
}
}

View File

@@ -0,0 +1,165 @@
package convert
import (
"net/url"
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"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"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
func AppToPb(query_app *query.App) *app.Application {
if query_app == nil {
return &app.Application{}
}
return &app.Application{
Id: query_app.ID,
CreationDate: timestamppb.New(query_app.CreationDate),
ChangeDate: timestamppb.New(query_app.ChangeDate),
State: appStateToPb(query_app.State),
Name: query_app.Name,
Config: appConfigToPb(query_app),
}
}
func AppsToPb(queryApps []*query.App) []*app.Application {
pbApps := make([]*app.Application, len(queryApps))
for i, queryApp := range queryApps {
pbApps[i] = AppToPb(queryApp)
}
return pbApps
}
func ListApplicationsRequestToModel(sysDefaults systemdefaults.SystemDefaults, req *app.ListApplicationsRequest) (*query.AppSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination())
if err != nil {
return nil, err
}
queries, err := appQueriesToModel(req.GetFilters())
if err != nil {
return nil, err
}
projectQuery, err := query.NewAppProjectIDSearchQuery(req.GetProjectId())
if err != nil {
return nil, err
}
queries = append(queries, projectQuery)
return &query.AppSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: appSortingToColumn(req.GetSortingColumn()),
},
Queries: queries,
}, nil
}
func appSortingToColumn(sortingCriteria app.AppSorting) query.Column {
switch sortingCriteria {
case app.AppSorting_APP_SORT_BY_CHANGE_DATE:
return query.AppColumnChangeDate
case app.AppSorting_APP_SORT_BY_CREATION_DATE:
return query.AppColumnCreationDate
case app.AppSorting_APP_SORT_BY_NAME:
return query.AppColumnName
case app.AppSorting_APP_SORT_BY_STATE:
return query.AppColumnState
case app.AppSorting_APP_SORT_BY_ID:
fallthrough
default:
return query.AppColumnID
}
}
func appStateToPb(state domain.AppState) app.AppState {
switch state {
case domain.AppStateActive:
return app.AppState_APP_STATE_ACTIVE
case domain.AppStateInactive:
return app.AppState_APP_STATE_INACTIVE
case domain.AppStateRemoved:
return app.AppState_APP_STATE_REMOVED
case domain.AppStateUnspecified:
fallthrough
default:
return app.AppState_APP_STATE_UNSPECIFIED
}
}
func appConfigToPb(app *query.App) app.ApplicationConfig {
if app.OIDCConfig != nil {
return appOIDCConfigToPb(app.OIDCConfig)
}
if app.SAMLConfig != nil {
return appSAMLConfigToPb(app.SAMLConfig)
}
return appAPIConfigToPb(app.APIConfig)
}
func loginVersionToDomain(version *app.LoginVersion) (*domain.LoginVersion, *string, error) {
switch v := version.GetVersion().(type) {
case nil:
return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil
case *app.LoginVersion_LoginV1:
return gu.Ptr(domain.LoginVersion1), gu.Ptr(""), nil
case *app.LoginVersion_LoginV2:
_, err := url.Parse(v.LoginV2.GetBaseUri())
return gu.Ptr(domain.LoginVersion2), gu.Ptr(v.LoginV2.GetBaseUri()), err
default:
return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil
}
}
func loginVersionToPb(version domain.LoginVersion, baseURI *string) *app.LoginVersion {
switch version {
case domain.LoginVersionUnspecified:
return nil
case domain.LoginVersion1:
return &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}
case domain.LoginVersion2:
return &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: baseURI}}}
default:
return nil
}
}
func appQueriesToModel(queries []*app.ApplicationSearchFilter) (toReturn []query.SearchQuery, err error) {
toReturn = make([]query.SearchQuery, len(queries))
for i, query := range queries {
toReturn[i], err = appQueryToModel(query)
if err != nil {
return nil, err
}
}
return toReturn, nil
}
func appQueryToModel(appQuery *app.ApplicationSearchFilter) (query.SearchQuery, error) {
switch q := appQuery.GetFilter().(type) {
case *app.ApplicationSearchFilter_NameFilter:
return query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(q.NameFilter.GetMethod()), q.NameFilter.Name)
case *app.ApplicationSearchFilter_StateFilter:
return query.NewAppStateSearchQuery(domain.AppState(q.StateFilter))
case *app.ApplicationSearchFilter_ApiAppOnly:
return query.NewNotNullQuery(query.AppAPIConfigColumnAppID)
case *app.ApplicationSearchFilter_OidcAppOnly:
return query.NewNotNullQuery(query.AppOIDCConfigColumnAppID)
case *app.ApplicationSearchFilter_SamlAppOnly:
return query.NewNotNullQuery(query.AppSAMLConfigColumnAppID)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid")
}
}

View File

@@ -0,0 +1,520 @@
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"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
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 *app.Application
}{
{
testName: "full app conversion",
inputQueryApp: &query.App{
ID: "id",
CreationDate: now,
ChangeDate: now,
State: domain.AppStateActive,
Name: "test-app",
APIConfig: &query.APIApp{},
},
expectedPbApp: &app.Application{
Id: "id",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
State: app.AppState_APP_STATE_ACTIVE,
Name: "test-app",
Config: &app.Application_ApiConfig{
ApiConfig: &app.APIConfig{},
},
},
},
{
testName: "nil app",
inputQueryApp: nil,
expectedPbApp: &app.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 *app.ListApplicationsRequest
expectedResponse *query.AppSearchQueries
expectedError error
}{
{
testName: "invalid pagination limit",
req: &app.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: &app.ListApplicationsRequest{
ProjectId: "project1",
Pagination: &filter_pb_v2.PaginationRequest{Asc: true},
},
expectedResponse: &query.AppSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 100,
Asc: true,
SortingColumn: query.AppColumnID,
},
Queries: []query.SearchQuery{
validSearchByProjectQuery,
},
},
},
{
testName: "valid request",
req: &app.ListApplicationsRequest{
ProjectId: "project1",
Filters: []*app.ApplicationSearchFilter{
{
Filter: &app.ApplicationSearchFilter_NameFilter{NameFilter: &app.ApplicationNameQuery{Name: "test"}},
},
},
SortingColumn: app.AppSorting_APP_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{
validSearchByNameQuery,
validSearchByProjectQuery,
},
},
},
}
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 TestAppSortingToColumn(t *testing.T) {
t.Parallel()
tt := []struct {
name string
sorting app.AppSorting
expected query.Column
}{
{
name: "sort by change date",
sorting: app.AppSorting_APP_SORT_BY_CHANGE_DATE,
expected: query.AppColumnChangeDate,
},
{
name: "sort by creation date",
sorting: app.AppSorting_APP_SORT_BY_CREATION_DATE,
expected: query.AppColumnCreationDate,
},
{
name: "sort by name",
sorting: app.AppSorting_APP_SORT_BY_NAME,
expected: query.AppColumnName,
},
{
name: "sort by state",
sorting: app.AppSorting_APP_SORT_BY_STATE,
expected: query.AppColumnState,
},
{
name: "sort by ID",
sorting: app.AppSorting_APP_SORT_BY_ID,
expected: query.AppColumnID,
},
{
name: "unknown sorting defaults to ID",
sorting: app.AppSorting(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 app.AppState
}{
{
name: "active state",
state: domain.AppStateActive,
expected: app.AppState_APP_STATE_ACTIVE,
},
{
name: "inactive state",
state: domain.AppStateInactive,
expected: app.AppState_APP_STATE_INACTIVE,
},
{
name: "removed state",
state: domain.AppStateRemoved,
expected: app.AppState_APP_STATE_REMOVED,
},
{
name: "unspecified state",
state: domain.AppStateUnspecified,
expected: app.AppState_APP_STATE_UNSPECIFIED,
},
{
name: "unknown state defaults to unspecified",
state: domain.AppState(99),
expected: app.AppState_APP_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 app.ApplicationConfig
}{
{
name: "OIDC config",
app: &query.App{
OIDCConfig: &query.OIDCApp{},
},
expected: &app.Application_OidcConfig{
OidcConfig: &app.OIDCConfig{
ResponseTypes: []app.OIDCResponseType{},
GrantTypes: []app.OIDCGrantType{},
ComplianceProblems: []*app.OIDCLocalizedMessage{},
ClockSkew: &durationpb.Duration{},
},
},
},
{
name: "SAML config",
app: &query.App{
SAMLConfig: &query.SAMLApp{},
},
expected: &app.Application_SamlConfig{
SamlConfig: &app.SAMLConfig{
Metadata: &app.SAMLConfig_MetadataXml{},
},
},
},
{
name: "API config",
app: &query.App{
APIConfig: &query.APIApp{},
},
expected: &app.Application_ApiConfig{
ApiConfig: &app.APIConfig{},
},
},
}
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 *app.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: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}},
expectedVer: gu.Ptr(domain.LoginVersion1),
expectedURI: gu.Ptr(""),
},
{
name: "login v2 valid URI",
version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://valid.url")}}},
expectedVer: gu.Ptr(domain.LoginVersion2),
expectedURI: gu.Ptr("https://valid.url"),
},
{
name: "login v2 invalid URI",
version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.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: &app.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 *app.LoginVersion
}{
{
name: "unspecified version",
version: domain.LoginVersionUnspecified,
baseURI: nil,
expected: nil,
},
{
name: "login v1",
version: domain.LoginVersion1,
baseURI: nil,
expected: &app.LoginVersion{
Version: &app.LoginVersion_LoginV1{
LoginV1: &app.LoginV1{},
},
},
},
{
name: "login v2",
version: domain.LoginVersion2,
baseURI: gu.Ptr("https://example.com"),
expected: &app.LoginVersion{
Version: &app.LoginVersion_LoginV2{
LoginV2: &app.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 *app.ApplicationSearchFilter
expectedQuery query.SearchQuery
expectedError error
}{
{
name: "name query",
query: &app.ApplicationSearchFilter{
Filter: &app.ApplicationSearchFilter_NameFilter{
NameFilter: &app.ApplicationNameQuery{
Name: "test",
Method: filter_pb_v2.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
expectedQuery: validAppNameSearchQuery,
},
{
name: "state query",
query: &app.ApplicationSearchFilter{
Filter: &app.ApplicationSearchFilter_StateFilter{
StateFilter: app.AppState_APP_STATE_ACTIVE,
},
},
expectedQuery: validAppStateSearchQuery,
},
{
name: "api app only query",
query: &app.ApplicationSearchFilter{
Filter: &app.ApplicationSearchFilter_ApiAppOnly{},
},
expectedQuery: &query.NotNullQuery{
Column: query.AppAPIConfigColumnAppID,
},
},
{
name: "oidc app only query",
query: &app.ApplicationSearchFilter{
Filter: &app.ApplicationSearchFilter_OidcAppOnly{},
},
expectedQuery: &query.NotNullQuery{
Column: query.AppOIDCConfigColumnAppID,
},
},
{
name: "saml app only query",
query: &app.ApplicationSearchFilter{
Filter: &app.ApplicationSearchFilter_SamlAppOnly{},
},
expectedQuery: &query.NotNullQuery{
Column: query.AppSAMLConfigColumnAppID,
},
},
{
name: "invalid query type",
query: &app.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 := appQueryToModel(tc.query)
// Then
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedQuery, result)
})
}
}

View File

@@ -0,0 +1,291 @@
package convert
import (
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
func CreateOIDCAppRequestToDomain(name, projectID string, req *app.CreateOIDCApplicationRequest) (*domain.OIDCApp, error) {
loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion())
if err != nil {
return nil, err
}
return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
},
AppName: name,
OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
RedirectUris: req.GetRedirectUris(),
ResponseTypes: oidcResponseTypesToDomain(req.GetResponseTypes()),
GrantTypes: oidcGrantTypesToDomain(req.GetGrantTypes()),
ApplicationType: gu.Ptr(oidcApplicationTypeToDomain(req.GetAppType())),
AuthMethodType: gu.Ptr(oidcAuthMethodTypeToDomain(req.GetAuthMethodType())),
PostLogoutRedirectUris: req.GetPostLogoutRedirectUris(),
DevMode: &req.DevMode,
AccessTokenType: gu.Ptr(oidcTokenTypeToDomain(req.GetAccessTokenType())),
AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()),
IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()),
IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()),
ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()),
AdditionalOrigins: req.GetAdditionalOrigins(),
SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()),
BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI,
}, nil
}
func UpdateOIDCAppConfigRequestToDomain(appID, projectID string, app *app.UpdateOIDCApplicationConfigurationRequest) (*domain.OIDCApp, error) {
loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion())
if err != nil {
return nil, err
}
return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
},
AppID: appID,
RedirectUris: app.RedirectUris,
ResponseTypes: oidcResponseTypesToDomain(app.ResponseTypes),
GrantTypes: oidcGrantTypesToDomain(app.GrantTypes),
ApplicationType: oidcApplicationTypeToDomainPtr(app.AppType),
AuthMethodType: oidcAuthMethodTypeToDomainPtr(app.AuthMethodType),
PostLogoutRedirectUris: app.PostLogoutRedirectUris,
DevMode: app.DevMode,
AccessTokenType: oidcTokenTypeToDomainPtr(app.AccessTokenType),
AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
IDTokenRoleAssertion: app.IdTokenRoleAssertion,
IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion,
ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()),
AdditionalOrigins: app.AdditionalOrigins,
SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
BackChannelLogoutURI: app.BackChannelLogoutUri,
LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI,
}, nil
}
func oidcResponseTypesToDomain(responseTypes []app.OIDCResponseType) []domain.OIDCResponseType {
if len(responseTypes) == 0 {
return []domain.OIDCResponseType{domain.OIDCResponseTypeCode}
}
oidcResponseTypes := make([]domain.OIDCResponseType, len(responseTypes))
for i, responseType := range responseTypes {
switch responseType {
case app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED:
oidcResponseTypes[i] = domain.OIDCResponseTypeUnspecified
case app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE:
oidcResponseTypes[i] = domain.OIDCResponseTypeCode
case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN:
oidcResponseTypes[i] = domain.OIDCResponseTypeIDToken
case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN:
oidcResponseTypes[i] = domain.OIDCResponseTypeIDTokenToken
}
}
return oidcResponseTypes
}
func oidcGrantTypesToDomain(grantTypes []app.OIDCGrantType) []domain.OIDCGrantType {
if len(grantTypes) == 0 {
return []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}
}
oidcGrantTypes := make([]domain.OIDCGrantType, len(grantTypes))
for i, grantType := range grantTypes {
switch grantType {
case app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE:
oidcGrantTypes[i] = domain.OIDCGrantTypeAuthorizationCode
case app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT:
oidcGrantTypes[i] = domain.OIDCGrantTypeImplicit
case app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN:
oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken
case app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE:
oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode
case app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE:
oidcGrantTypes[i] = domain.OIDCGrantTypeTokenExchange
}
}
return oidcGrantTypes
}
func oidcApplicationTypeToDomainPtr(appType *app.OIDCAppType) *domain.OIDCApplicationType {
if appType == nil {
return nil
}
res := oidcApplicationTypeToDomain(*appType)
return &res
}
func oidcApplicationTypeToDomain(appType app.OIDCAppType) domain.OIDCApplicationType {
switch appType {
case app.OIDCAppType_OIDC_APP_TYPE_WEB:
return domain.OIDCApplicationTypeWeb
case app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT:
return domain.OIDCApplicationTypeUserAgent
case app.OIDCAppType_OIDC_APP_TYPE_NATIVE:
return domain.OIDCApplicationTypeNative
}
return domain.OIDCApplicationTypeWeb
}
func oidcAuthMethodTypeToDomainPtr(authType *app.OIDCAuthMethodType) *domain.OIDCAuthMethodType {
if authType == nil {
return nil
}
res := oidcAuthMethodTypeToDomain(*authType)
return &res
}
func oidcAuthMethodTypeToDomain(authType app.OIDCAuthMethodType) domain.OIDCAuthMethodType {
switch authType {
case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC:
return domain.OIDCAuthMethodTypeBasic
case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST:
return domain.OIDCAuthMethodTypePost
case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE:
return domain.OIDCAuthMethodTypeNone
case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT:
return domain.OIDCAuthMethodTypePrivateKeyJWT
default:
return domain.OIDCAuthMethodTypeBasic
}
}
func oidcTokenTypeToDomainPtr(tokenType *app.OIDCTokenType) *domain.OIDCTokenType {
if tokenType == nil {
return nil
}
res := oidcTokenTypeToDomain(*tokenType)
return &res
}
func oidcTokenTypeToDomain(tokenType app.OIDCTokenType) domain.OIDCTokenType {
switch tokenType {
case app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER:
return domain.OIDCTokenTypeBearer
case app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT:
return domain.OIDCTokenTypeJWT
default:
return domain.OIDCTokenTypeBearer
}
}
func ComplianceProblemsToLocalizedMessages(complianceProblems []string) []*app.OIDCLocalizedMessage {
converted := make([]*app.OIDCLocalizedMessage, len(complianceProblems))
for i, p := range complianceProblems {
converted[i] = &app.OIDCLocalizedMessage{Key: p}
}
return converted
}
func appOIDCConfigToPb(oidcApp *query.OIDCApp) *app.Application_OidcConfig {
return &app.Application_OidcConfig{
OidcConfig: &app.OIDCConfig{
RedirectUris: oidcApp.RedirectURIs,
ResponseTypes: oidcResponseTypesFromModel(oidcApp.ResponseTypes),
GrantTypes: oidcGrantTypesFromModel(oidcApp.GrantTypes),
AppType: oidcApplicationTypeToPb(oidcApp.AppType),
ClientId: oidcApp.ClientID,
AuthMethodType: oidcAuthMethodTypeToPb(oidcApp.AuthMethodType),
PostLogoutRedirectUris: oidcApp.PostLogoutRedirectURIs,
Version: app.OIDCVersion_OIDC_VERSION_1_0,
NoneCompliant: len(oidcApp.ComplianceProblems) != 0,
ComplianceProblems: ComplianceProblemsToLocalizedMessages(oidcApp.ComplianceProblems),
DevMode: oidcApp.IsDevMode,
AccessTokenType: oidcTokenTypeToPb(oidcApp.AccessTokenType),
AccessTokenRoleAssertion: oidcApp.AssertAccessTokenRole,
IdTokenRoleAssertion: oidcApp.AssertIDTokenRole,
IdTokenUserinfoAssertion: oidcApp.AssertIDTokenUserinfo,
ClockSkew: durationpb.New(oidcApp.ClockSkew),
AdditionalOrigins: oidcApp.AdditionalOrigins,
AllowedOrigins: oidcApp.AllowedOrigins,
SkipNativeAppSuccessPage: oidcApp.SkipNativeAppSuccessPage,
BackChannelLogoutUri: oidcApp.BackChannelLogoutURI,
LoginVersion: loginVersionToPb(oidcApp.LoginVersion, oidcApp.LoginBaseURI),
},
}
}
func oidcResponseTypesFromModel(responseTypes []domain.OIDCResponseType) []app.OIDCResponseType {
oidcResponseTypes := make([]app.OIDCResponseType, len(responseTypes))
for i, responseType := range responseTypes {
switch responseType {
case domain.OIDCResponseTypeUnspecified:
oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED
case domain.OIDCResponseTypeCode:
oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE
case domain.OIDCResponseTypeIDToken:
oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN
case domain.OIDCResponseTypeIDTokenToken:
oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN
}
}
return oidcResponseTypes
}
func oidcGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app.OIDCGrantType {
oidcGrantTypes := make([]app.OIDCGrantType, len(grantTypes))
for i, grantType := range grantTypes {
switch grantType {
case domain.OIDCGrantTypeAuthorizationCode:
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE
case domain.OIDCGrantTypeImplicit:
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT
case domain.OIDCGrantTypeRefreshToken:
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN
case domain.OIDCGrantTypeDeviceCode:
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE
case domain.OIDCGrantTypeTokenExchange:
oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE
}
}
return oidcGrantTypes
}
func oidcApplicationTypeToPb(appType domain.OIDCApplicationType) app.OIDCAppType {
switch appType {
case domain.OIDCApplicationTypeWeb:
return app.OIDCAppType_OIDC_APP_TYPE_WEB
case domain.OIDCApplicationTypeUserAgent:
return app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT
case domain.OIDCApplicationTypeNative:
return app.OIDCAppType_OIDC_APP_TYPE_NATIVE
default:
return app.OIDCAppType_OIDC_APP_TYPE_WEB
}
}
func oidcAuthMethodTypeToPb(authType domain.OIDCAuthMethodType) app.OIDCAuthMethodType {
switch authType {
case domain.OIDCAuthMethodTypeBasic:
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC
case domain.OIDCAuthMethodTypePost:
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST
case domain.OIDCAuthMethodTypeNone:
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE
case domain.OIDCAuthMethodTypePrivateKeyJWT:
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
default:
return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC
}
}
func oidcTokenTypeToPb(tokenType domain.OIDCTokenType) app.OIDCTokenType {
switch tokenType {
case domain.OIDCTokenTypeBearer:
return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER
case domain.OIDCTokenTypeJWT:
return app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT
default:
return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER
}
}

View File

@@ -0,0 +1,755 @@
package convert
import (
"net/url"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
func TestCreateOIDCAppRequestToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
projectID string
req *app.CreateOIDCApplicationRequest
expectedModel *domain.OIDCApp
expectedError error
}{
{
testName: "unparsable login version 2 URL",
projectID: "pid",
req: &app.CreateOIDCApplicationRequest{
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}},
},
},
expectedModel: nil,
expectedError: &url.Error{
URL: "%+o",
Op: "parse",
Err: url.EscapeError("%+o"),
},
},
{
testName: "all fields set",
projectID: "project1",
req: &app.CreateOIDCApplicationRequest{
RedirectUris: []string{"https://redirect"},
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
PostLogoutRedirectUris: []string{"https://logout"},
DevMode: true,
AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
AccessTokenRoleAssertion: true,
IdTokenRoleAssertion: true,
IdTokenUserinfoAssertion: true,
ClockSkew: durationpb.New(5 * time.Second),
AdditionalOrigins: []string{"https://origin"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutUri: "https://backchannel",
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{
BaseUri: gu.Ptr("https://login"),
}}},
},
expectedModel: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{AggregateID: "project1"},
AppName: "all fields set",
OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
RedirectUris: []string{"https://redirect"},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
PostLogoutRedirectUris: []string{"https://logout"},
DevMode: gu.Ptr(true),
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
AccessTokenRoleAssertion: gu.Ptr(true),
IDTokenRoleAssertion: gu.Ptr(true),
IDTokenUserinfoAssertion: gu.Ptr(true),
ClockSkew: gu.Ptr(5 * time.Second),
AdditionalOrigins: []string{"https://origin"},
SkipNativeAppSuccessPage: gu.Ptr(true),
BackChannelLogoutURI: gu.Ptr("https://backchannel"),
LoginVersion: gu.Ptr(domain.LoginVersion2),
LoginBaseURI: gu.Ptr("https://login"),
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// When
res, err := CreateOIDCAppRequestToDomain(tc.testName, tc.projectID, tc.req)
// Then
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedModel, res)
})
}
}
func TestUpdateOIDCAppConfigRequestToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
appID string
projectID string
req *app.UpdateOIDCApplicationConfigurationRequest
expectedModel *domain.OIDCApp
expectedError error
}{
{
testName: "unparsable login version 2 URL",
appID: "app1",
projectID: "pid",
req: &app.UpdateOIDCApplicationConfigurationRequest{
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")},
}},
},
expectedModel: nil,
expectedError: &url.Error{
URL: "%+o",
Op: "parse",
Err: url.EscapeError("%+o"),
},
},
{
testName: "successful Update",
appID: "app1",
projectID: "proj1",
req: &app.UpdateOIDCApplicationConfigurationRequest{
RedirectUris: []string{"https://redirect"},
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
AppType: gu.Ptr(app.OIDCAppType_OIDC_APP_TYPE_WEB),
AuthMethodType: gu.Ptr(app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC),
PostLogoutRedirectUris: []string{"https://logout"},
DevMode: gu.Ptr(true),
AccessTokenType: gu.Ptr(app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER),
AccessTokenRoleAssertion: gu.Ptr(true),
IdTokenRoleAssertion: gu.Ptr(true),
IdTokenUserinfoAssertion: gu.Ptr(true),
ClockSkew: durationpb.New(5 * time.Second),
AdditionalOrigins: []string{"https://origin"},
SkipNativeAppSuccessPage: gu.Ptr(true),
BackChannelLogoutUri: gu.Ptr("https://backchannel"),
LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://login")},
}},
},
expectedModel: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj1"},
AppID: "app1",
RedirectUris: []string{"https://redirect"},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
PostLogoutRedirectUris: []string{"https://logout"},
DevMode: gu.Ptr(true),
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
AccessTokenRoleAssertion: gu.Ptr(true),
IDTokenRoleAssertion: gu.Ptr(true),
IDTokenUserinfoAssertion: gu.Ptr(true),
ClockSkew: gu.Ptr(5 * time.Second),
AdditionalOrigins: []string{"https://origin"},
SkipNativeAppSuccessPage: gu.Ptr(true),
BackChannelLogoutURI: gu.Ptr("https://backchannel"),
LoginVersion: gu.Ptr(domain.LoginVersion2),
LoginBaseURI: gu.Ptr("https://login"),
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// When
got, err := UpdateOIDCAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req)
// Then
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedModel, got)
})
}
}
func TestOIDCResponseTypesToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputResponseType []app.OIDCResponseType
expectedResponse []domain.OIDCResponseType
}{
{
testName: "empty response types",
inputResponseType: []app.OIDCResponseType{},
expectedResponse: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
},
{
testName: "all response types",
inputResponseType: []app.OIDCResponseType{
app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED,
app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN,
app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN,
},
expectedResponse: []domain.OIDCResponseType{
domain.OIDCResponseTypeUnspecified,
domain.OIDCResponseTypeCode,
domain.OIDCResponseTypeIDToken,
domain.OIDCResponseTypeIDTokenToken,
},
},
{
testName: "single response type",
inputResponseType: []app.OIDCResponseType{
app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
},
expectedResponse: []domain.OIDCResponseType{
domain.OIDCResponseTypeCode,
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// When
res := oidcResponseTypesToDomain(tc.inputResponseType)
// Then
assert.Equal(t, tc.expectedResponse, res)
})
}
}
func TestOIDCGrantTypesToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputGrantType []app.OIDCGrantType
expectedGrants []domain.OIDCGrantType
}{
{
testName: "empty grant types",
inputGrantType: []app.OIDCGrantType{},
expectedGrants: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
},
{
testName: "all grant types",
inputGrantType: []app.OIDCGrantType{
app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT,
app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN,
app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE,
app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
},
expectedGrants: []domain.OIDCGrantType{
domain.OIDCGrantTypeAuthorizationCode,
domain.OIDCGrantTypeImplicit,
domain.OIDCGrantTypeRefreshToken,
domain.OIDCGrantTypeDeviceCode,
domain.OIDCGrantTypeTokenExchange,
},
},
{
testName: "single grant type",
inputGrantType: []app.OIDCGrantType{
app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
},
expectedGrants: []domain.OIDCGrantType{
domain.OIDCGrantTypeAuthorizationCode,
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// When
res := oidcGrantTypesToDomain(tc.inputGrantType)
// Then
assert.Equal(t, tc.expectedGrants, res)
})
}
}
func TestOIDCApplicationTypeToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
name string
appType app.OIDCAppType
expected domain.OIDCApplicationType
}{
{
name: "web type",
appType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
expected: domain.OIDCApplicationTypeWeb,
},
{
name: "user agent type",
appType: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT,
expected: domain.OIDCApplicationTypeUserAgent,
},
{
name: "native type",
appType: app.OIDCAppType_OIDC_APP_TYPE_NATIVE,
expected: domain.OIDCApplicationTypeNative,
},
{
name: "unspecified type defaults to web",
expected: domain.OIDCApplicationTypeWeb,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := oidcApplicationTypeToDomain(tc.appType)
// Then
assert.Equal(t, tc.expected, result)
})
}
}
func TestOIDCAuthMethodTypeToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
name string
authType app.OIDCAuthMethodType
expectedResponse domain.OIDCAuthMethodType
}{
{
name: "basic auth type",
authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
expectedResponse: domain.OIDCAuthMethodTypeBasic,
},
{
name: "post auth type",
authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST,
expectedResponse: domain.OIDCAuthMethodTypePost,
},
{
name: "none auth type",
authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
expectedResponse: domain.OIDCAuthMethodTypeNone,
},
{
name: "private key jwt auth type",
authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
expectedResponse: domain.OIDCAuthMethodTypePrivateKeyJWT,
},
{
name: "unspecified auth type defaults to basic",
expectedResponse: domain.OIDCAuthMethodTypeBasic,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
res := oidcAuthMethodTypeToDomain(tc.authType)
// Then
assert.Equal(t, tc.expectedResponse, res)
})
}
}
func TestOIDCTokenTypeToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
name string
tokenType app.OIDCTokenType
expectedType domain.OIDCTokenType
}{
{
name: "bearer token type",
tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
expectedType: domain.OIDCTokenTypeBearer,
},
{
name: "jwt token type",
tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
expectedType: domain.OIDCTokenTypeJWT,
},
{
name: "unspecified defaults to bearer",
expectedType: domain.OIDCTokenTypeBearer,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := oidcTokenTypeToDomain(tc.tokenType)
// Then
assert.Equal(t, tc.expectedType, result)
})
}
}
func TestAppOIDCConfigToPb(t *testing.T) {
t.Parallel()
tt := []struct {
name string
input *query.OIDCApp
expected *app.Application_OidcConfig
}{
{
name: "empty config",
input: &query.OIDCApp{},
expected: &app.Application_OidcConfig{
OidcConfig: &app.OIDCConfig{
Version: app.OIDCVersion_OIDC_VERSION_1_0,
ComplianceProblems: []*app.OIDCLocalizedMessage{},
ClockSkew: durationpb.New(0),
ResponseTypes: []app.OIDCResponseType{},
GrantTypes: []app.OIDCGrantType{},
},
},
},
{
name: "full config",
input: &query.OIDCApp{
RedirectURIs: []string{"https://example.com/callback"},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
AppType: domain.OIDCApplicationTypeWeb,
ClientID: "client123",
AuthMethodType: domain.OIDCAuthMethodTypeBasic,
PostLogoutRedirectURIs: []string{"https://example.com/logout"},
ComplianceProblems: []string{"problem1", "problem2"},
IsDevMode: true,
AccessTokenType: domain.OIDCTokenTypeBearer,
AssertAccessTokenRole: true,
AssertIDTokenRole: true,
AssertIDTokenUserinfo: true,
ClockSkew: 5 * time.Second,
AdditionalOrigins: []string{"https://app.example.com"},
AllowedOrigins: []string{"https://allowed.example.com"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: "https://example.com/backchannel",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: gu.Ptr("https://login.example.com"),
},
expected: &app.Application_OidcConfig{
OidcConfig: &app.OIDCConfig{
RedirectUris: []string{"https://example.com/callback"},
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
ClientId: "client123",
AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
PostLogoutRedirectUris: []string{"https://example.com/logout"},
Version: app.OIDCVersion_OIDC_VERSION_1_0,
NoneCompliant: true,
ComplianceProblems: []*app.OIDCLocalizedMessage{
{Key: "problem1"},
{Key: "problem2"},
},
DevMode: true,
AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
AccessTokenRoleAssertion: true,
IdTokenRoleAssertion: true,
IdTokenUserinfoAssertion: true,
ClockSkew: durationpb.New(5 * time.Second),
AdditionalOrigins: []string{"https://app.example.com"},
AllowedOrigins: []string{"https://allowed.example.com"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutUri: "https://example.com/backchannel",
LoginVersion: &app.LoginVersion{
Version: &app.LoginVersion_LoginV2{
LoginV2: &app.LoginV2{
BaseUri: gu.Ptr("https://login.example.com"),
},
},
},
},
},
},
}
for _, tt := range tt {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// When
result := appOIDCConfigToPb(tt.input)
// Then
assert.Equal(t, tt.expected, result)
})
}
}
func TestOIDCResponseTypesFromModel(t *testing.T) {
t.Parallel()
tt := []struct {
name string
responseTypes []domain.OIDCResponseType
expected []app.OIDCResponseType
}{
{
name: "empty response types",
responseTypes: []domain.OIDCResponseType{},
expected: []app.OIDCResponseType{},
},
{
name: "all response types",
responseTypes: []domain.OIDCResponseType{
domain.OIDCResponseTypeUnspecified,
domain.OIDCResponseTypeCode,
domain.OIDCResponseTypeIDToken,
domain.OIDCResponseTypeIDTokenToken,
},
expected: []app.OIDCResponseType{
app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED,
app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN,
app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN,
},
},
{
name: "single response type",
responseTypes: []domain.OIDCResponseType{
domain.OIDCResponseTypeCode,
},
expected: []app.OIDCResponseType{
app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE,
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := oidcResponseTypesFromModel(tc.responseTypes)
// Then
assert.Equal(t, tc.expected, result)
})
}
}
func TestOIDCGrantTypesFromModel(t *testing.T) {
t.Parallel()
tt := []struct {
name string
grantTypes []domain.OIDCGrantType
expected []app.OIDCGrantType
}{
{
name: "empty grant types",
grantTypes: []domain.OIDCGrantType{},
expected: []app.OIDCGrantType{},
},
{
name: "all grant types",
grantTypes: []domain.OIDCGrantType{
domain.OIDCGrantTypeAuthorizationCode,
domain.OIDCGrantTypeImplicit,
domain.OIDCGrantTypeRefreshToken,
domain.OIDCGrantTypeDeviceCode,
domain.OIDCGrantTypeTokenExchange,
},
expected: []app.OIDCGrantType{
app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT,
app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN,
app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE,
app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
},
},
{
name: "single grant type",
grantTypes: []domain.OIDCGrantType{
domain.OIDCGrantTypeAuthorizationCode,
},
expected: []app.OIDCGrantType{
app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := oidcGrantTypesFromModel(tc.grantTypes)
// Then
assert.Equal(t, tc.expected, result)
})
}
}
func TestOIDCApplicationTypeToPb(t *testing.T) {
t.Parallel()
tt := []struct {
name string
appType domain.OIDCApplicationType
expected app.OIDCAppType
}{
{
name: "web type",
appType: domain.OIDCApplicationTypeWeb,
expected: app.OIDCAppType_OIDC_APP_TYPE_WEB,
},
{
name: "user agent type",
appType: domain.OIDCApplicationTypeUserAgent,
expected: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT,
},
{
name: "native type",
appType: domain.OIDCApplicationTypeNative,
expected: app.OIDCAppType_OIDC_APP_TYPE_NATIVE,
},
{
name: "unspecified type defaults to web",
appType: domain.OIDCApplicationType(999), // Invalid value to trigger default case
expected: app.OIDCAppType_OIDC_APP_TYPE_WEB,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := oidcApplicationTypeToPb(tc.appType)
// Then
assert.Equal(t, tc.expected, result)
})
}
}
func TestOIDCAuthMethodTypeToPb(t *testing.T) {
t.Parallel()
tt := []struct {
name string
authType domain.OIDCAuthMethodType
expected app.OIDCAuthMethodType
}{
{
name: "basic auth type",
authType: domain.OIDCAuthMethodTypeBasic,
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
},
{
name: "post auth type",
authType: domain.OIDCAuthMethodTypePost,
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST,
},
{
name: "none auth type",
authType: domain.OIDCAuthMethodTypeNone,
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
},
{
name: "private key jwt auth type",
authType: domain.OIDCAuthMethodTypePrivateKeyJWT,
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
},
{
name: "unknown auth type defaults to basic",
authType: domain.OIDCAuthMethodType(999),
expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := oidcAuthMethodTypeToPb(tc.authType)
// Then
assert.Equal(t, tc.expected, result)
})
}
}
func TestOIDCTokenTypeToPb(t *testing.T) {
t.Parallel()
tt := []struct {
name string
tokenType domain.OIDCTokenType
expected app.OIDCTokenType
}{
{
name: "bearer token type",
tokenType: domain.OIDCTokenTypeBearer,
expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
},
{
name: "jwt token type",
tokenType: domain.OIDCTokenTypeJWT,
expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
},
{
name: "unknown token type defaults to bearer",
tokenType: domain.OIDCTokenType(999), // Invalid value to trigger default case
expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
result := oidcTokenTypeToPb(tc.tokenType)
// Then
assert.Equal(t, tc.expected, result)
})
}
}

View File

@@ -0,0 +1,77 @@
package convert
import (
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
func CreateSAMLAppRequestToDomain(name, projectID string, req *app.CreateSAMLApplicationRequest) (*domain.SAMLApp, error) {
loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion())
if err != nil {
return nil, err
}
return &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
},
AppName: name,
Metadata: req.GetMetadataXml(),
MetadataURL: gu.Ptr(req.GetMetadataUrl()),
LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI,
}, nil
}
func UpdateSAMLAppConfigRequestToDomain(appID, projectID string, app *app.UpdateSAMLApplicationConfigurationRequest) (*domain.SAMLApp, error) {
loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion())
if err != nil {
return nil, err
}
metasXML, metasURL := metasToDomain(app.GetMetadata())
return &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
},
AppID: appID,
Metadata: metasXML,
MetadataURL: metasURL,
LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI,
}, nil
}
func metasToDomain(metas app.MetaType) ([]byte, *string) {
switch t := metas.(type) {
case *app.UpdateSAMLApplicationConfigurationRequest_MetadataXml:
return t.MetadataXml, nil
case *app.UpdateSAMLApplicationConfigurationRequest_MetadataUrl:
return nil, &t.MetadataUrl
case nil:
return nil, nil
default:
return nil, nil
}
}
func appSAMLConfigToPb(samlApp *query.SAMLApp) app.ApplicationConfig {
if samlApp == nil {
return &app.Application_SamlConfig{
SamlConfig: &app.SAMLConfig{
Metadata: &app.SAMLConfig_MetadataXml{},
LoginVersion: &app.LoginVersion{},
},
}
}
return &app.Application_SamlConfig{
SamlConfig: &app.SAMLConfig{
Metadata: &app.SAMLConfig_MetadataXml{MetadataXml: samlApp.Metadata},
LoginVersion: loginVersionToPb(samlApp.LoginVersion, samlApp.LoginBaseURI),
},
}
}

View File

@@ -0,0 +1,256 @@
package convert
import (
"fmt"
"net/url"
"testing"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
)
func samlMetadataGen(entityID string) []byte {
str := fmt.Sprintf(`<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
validUntil="2022-08-26T14:08:16Z"
cacheDuration="PT604800S"
entityID="%s">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://test.com/saml/acs"
index="1" />
</md:SPSSODescriptor>
</md:EntityDescriptor>
`,
entityID)
return []byte(str)
}
func TestCreateSAMLAppRequestToDomain(t *testing.T) {
t.Parallel()
genMetaForValidRequest := samlMetadataGen(gofakeit.URL())
tt := []struct {
testName string
appName string
projectID string
req *app.CreateSAMLApplicationRequest
expectedResponse *domain.SAMLApp
expectedError error
}{
{
testName: "login version error",
appName: "test-app",
projectID: "proj-1",
req: &app.CreateSAMLApplicationRequest{
Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
MetadataXml: samlMetadataGen(gofakeit.URL()),
},
LoginVersion: &app.LoginVersion{
Version: &app.LoginVersion_LoginV2{
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")},
},
},
},
expectedError: &url.Error{
URL: "%+o",
Op: "parse",
Err: url.EscapeError("%+o"),
},
},
{
testName: "valid request",
appName: "test-app",
projectID: "proj-1",
req: &app.CreateSAMLApplicationRequest{
Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{
MetadataXml: genMetaForValidRequest,
},
LoginVersion: nil,
},
expectedResponse: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppName: "test-app",
Metadata: genMetaForValidRequest,
MetadataURL: gu.Ptr(""),
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
State: 0,
},
},
{
testName: "nil request",
appName: "test-app",
projectID: "proj-1",
req: nil,
expectedResponse: &domain.SAMLApp{
AppName: "test-app",
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
MetadataURL: gu.Ptr(""),
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// When
res, err := CreateSAMLAppRequestToDomain(tc.appName, tc.projectID, tc.req)
// Then
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedResponse, res)
})
}
}
func TestUpdateSAMLAppConfigRequestToDomain(t *testing.T) {
t.Parallel()
genMetaForValidRequest := samlMetadataGen(gofakeit.URL())
tt := []struct {
testName string
appID string
projectID string
req *app.UpdateSAMLApplicationConfigurationRequest
expectedResponse *domain.SAMLApp
expectedError error
}{
{
testName: "login version error",
appID: "app-1",
projectID: "proj-1",
req: &app.UpdateSAMLApplicationConfigurationRequest{
Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
MetadataXml: samlMetadataGen(gofakeit.URL()),
},
LoginVersion: &app.LoginVersion{
Version: &app.LoginVersion_LoginV2{
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")},
},
},
},
expectedError: &url.Error{
URL: "%+o",
Op: "parse",
Err: url.EscapeError("%+o"),
},
},
{
testName: "valid request",
appID: "app-1",
projectID: "proj-1",
req: &app.UpdateSAMLApplicationConfigurationRequest{
Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{
MetadataXml: genMetaForValidRequest,
},
LoginVersion: nil,
},
expectedResponse: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppID: "app-1",
Metadata: genMetaForValidRequest,
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
},
},
{
testName: "nil request",
appID: "app-1",
projectID: "proj-1",
req: nil,
expectedResponse: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"},
AppID: "app-1",
LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
LoginBaseURI: gu.Ptr(""),
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// When
res, err := UpdateSAMLAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req)
// Then
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedResponse, res)
})
}
}
func TestAppSAMLConfigToPb(t *testing.T) {
t.Parallel()
metadata := samlMetadataGen(gofakeit.URL())
tt := []struct {
name string
inputSAMLApp *query.SAMLApp
expectedPbApp app.ApplicationConfig
}{
{
name: "valid conversion",
inputSAMLApp: &query.SAMLApp{
Metadata: metadata,
LoginVersion: domain.LoginVersion2,
LoginBaseURI: gu.Ptr("https://example.com"),
},
expectedPbApp: &app.Application_SamlConfig{
SamlConfig: &app.SAMLConfig{
Metadata: &app.SAMLConfig_MetadataXml{
MetadataXml: metadata,
},
LoginVersion: &app.LoginVersion{
Version: &app.LoginVersion_LoginV2{
LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://example.com")},
},
},
},
},
},
{
name: "nil saml app",
inputSAMLApp: nil,
expectedPbApp: &app.Application_SamlConfig{
SamlConfig: &app.SAMLConfig{
Metadata: &app.SAMLConfig_MetadataXml{},
LoginVersion: &app.LoginVersion{},
},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// When
got := appSAMLConfigToPb(tc.inputSAMLApp)
// Then
assert.Equal(t, tc.expectedPbApp, got)
})
}
}