package test import ( "context" "testing" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/fieldmaskpb" v1pb "github.com/usememos/memos/proto/gen/api/v1" apiv1 "github.com/usememos/memos/server/router/api/v1" "github.com/usememos/memos/store" ) func TestCreateIdentityProvider(t *testing.T) { ctx := context.Background() t.Run("CreateIdentityProvider success", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context ctx := ts.CreateUserContext(ctx, hostUser.ID) // Create OAuth2 identity provider req := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Test OAuth2 Provider", IdentifierFilter: "", Type: v1pb.IdentityProvider_OAUTH2, Config: &v1pb.IdentityProviderConfig{ Config: &v1pb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &v1pb.OAuth2Config{ ClientId: "test-client-id", ClientSecret: "test-client-secret", AuthUrl: "https://example.com/oauth/authorize", TokenUrl: "https://example.com/oauth/token", UserInfoUrl: "https://example.com/oauth/userinfo", Scopes: []string{"openid", "profile", "email"}, FieldMapping: &v1pb.FieldMapping{ Identifier: "id", DisplayName: "name", Email: "email", AvatarUrl: "avatar_url", }, }, }, }, }, } resp, err := ts.Service.CreateIdentityProvider(ctx, req) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "Test OAuth2 Provider", resp.Title) require.Equal(t, v1pb.IdentityProvider_OAUTH2, resp.Type) require.Contains(t, resp.Name, "identity-providers/") require.NotNil(t, resp.Config.GetOauth2Config()) require.Equal(t, "test-client-id", resp.Config.GetOauth2Config().ClientId) }) t.Run("CreateIdentityProvider permission denied for non-host user", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create regular user regularUser, err := ts.CreateRegularUser(ctx, "user") require.NoError(t, err) // Set user context ctx := ts.CreateUserContext(ctx, regularUser.ID) req := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Test Provider", Type: v1pb.IdentityProvider_OAUTH2, }, } _, err = ts.Service.CreateIdentityProvider(ctx, req) require.Error(t, err) require.Contains(t, err.Error(), "permission denied") }) t.Run("CreateIdentityProvider unauthenticated", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() req := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Test Provider", Type: v1pb.IdentityProvider_OAUTH2, }, } _, err := ts.Service.CreateIdentityProvider(ctx, req) require.Error(t, err) require.Contains(t, err.Error(), "user not authenticated") }) } func TestListIdentityProviders(t *testing.T) { ctx := context.Background() t.Run("ListIdentityProviders empty", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() req := &v1pb.ListIdentityProvidersRequest{} resp, err := ts.Service.ListIdentityProviders(ctx, req) require.NoError(t, err) require.NotNil(t, resp) require.Empty(t, resp.IdentityProviders) }) t.Run("ListIdentityProviders with providers", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) // Create a couple of identity providers createReq1 := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Provider 1", Type: v1pb.IdentityProvider_OAUTH2, Config: &v1pb.IdentityProviderConfig{ Config: &v1pb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &v1pb.OAuth2Config{ ClientId: "client1", AuthUrl: "https://example1.com/auth", TokenUrl: "https://example1.com/token", UserInfoUrl: "https://example1.com/user", FieldMapping: &v1pb.FieldMapping{ Identifier: "id", }, }, }, }, }, } createReq2 := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Provider 2", Type: v1pb.IdentityProvider_OAUTH2, Config: &v1pb.IdentityProviderConfig{ Config: &v1pb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &v1pb.OAuth2Config{ ClientId: "client2", AuthUrl: "https://example2.com/auth", TokenUrl: "https://example2.com/token", UserInfoUrl: "https://example2.com/user", FieldMapping: &v1pb.FieldMapping{ Identifier: "id", }, }, }, }, }, } _, err = ts.Service.CreateIdentityProvider(userCtx, createReq1) require.NoError(t, err) _, err = ts.Service.CreateIdentityProvider(userCtx, createReq2) require.NoError(t, err) // List providers listReq := &v1pb.ListIdentityProvidersRequest{} resp, err := ts.Service.ListIdentityProviders(ctx, listReq) require.NoError(t, err) require.NotNil(t, resp) require.Len(t, resp.IdentityProviders, 2) // Verify response contains expected providers titles := []string{resp.IdentityProviders[0].Title, resp.IdentityProviders[1].Title} require.Contains(t, titles, "Provider 1") require.Contains(t, titles, "Provider 2") }) } func TestGetIdentityProvider(t *testing.T) { ctx := context.Background() t.Run("GetIdentityProvider success", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) // Create identity provider createReq := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Test Provider", Type: v1pb.IdentityProvider_OAUTH2, Config: &v1pb.IdentityProviderConfig{ Config: &v1pb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &v1pb.OAuth2Config{ ClientId: "test-client", ClientSecret: "test-secret", AuthUrl: "https://example.com/auth", TokenUrl: "https://example.com/token", UserInfoUrl: "https://example.com/user", Scopes: []string{"openid", "profile"}, FieldMapping: &v1pb.FieldMapping{ Identifier: "id", DisplayName: "name", Email: "email", }, }, }, }, }, } created, err := ts.Service.CreateIdentityProvider(userCtx, createReq) require.NoError(t, err) // Get identity provider getReq := &v1pb.GetIdentityProviderRequest{ Name: created.Name, } // ClientSecret is write-only: never returned in responses, even to admins. resp, err := ts.Service.GetIdentityProvider(ctx, getReq) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, created.Name, resp.Name) require.Equal(t, "Test Provider", resp.Title) require.Equal(t, v1pb.IdentityProvider_OAUTH2, resp.Type) require.NotNil(t, resp.Config.GetOauth2Config()) require.Equal(t, "test-client", resp.Config.GetOauth2Config().ClientId) require.Empty(t, resp.Config.GetOauth2Config().ClientSecret, "ClientSecret must never be returned in responses") // Same for admin: secret is still write-only. respAdmin, err := ts.Service.GetIdentityProvider(userCtx, getReq) require.NoError(t, err) require.NotNil(t, respAdmin) require.Equal(t, "test-client", respAdmin.Config.GetOauth2Config().ClientId) require.Empty(t, respAdmin.Config.GetOauth2Config().ClientSecret, "ClientSecret must never be returned in responses, even to admins") }) t.Run("GetIdentityProvider not found", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() req := &v1pb.GetIdentityProviderRequest{ Name: "identity-providers/999", } _, err := ts.Service.GetIdentityProvider(ctx, req) require.Error(t, err) require.Contains(t, err.Error(), "not found") }) t.Run("GetIdentityProvider invalid name", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() req := &v1pb.GetIdentityProviderRequest{ Name: "invalid-name", } _, err := ts.Service.GetIdentityProvider(ctx, req) require.Error(t, err) require.Contains(t, err.Error(), "invalid identity provider name") }) } func TestUpdateIdentityProvider(t *testing.T) { ctx := context.Background() t.Run("UpdateIdentityProvider success", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) // Create identity provider createReq := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Original Provider", IdentifierFilter: "", Type: v1pb.IdentityProvider_OAUTH2, Config: &v1pb.IdentityProviderConfig{ Config: &v1pb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &v1pb.OAuth2Config{ ClientId: "original-client", AuthUrl: "https://original.com/auth", TokenUrl: "https://original.com/token", UserInfoUrl: "https://original.com/user", FieldMapping: &v1pb.FieldMapping{ Identifier: "id", }, }, }, }, }, } created, err := ts.Service.CreateIdentityProvider(userCtx, createReq) require.NoError(t, err) // Update identity provider updateReq := &v1pb.UpdateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Name: created.Name, Title: "Updated Provider", IdentifierFilter: "test@example.com", Type: v1pb.IdentityProvider_OAUTH2, Config: &v1pb.IdentityProviderConfig{ Config: &v1pb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &v1pb.OAuth2Config{ ClientId: "updated-client", ClientSecret: "updated-secret", AuthUrl: "https://updated.com/auth", TokenUrl: "https://updated.com/token", UserInfoUrl: "https://updated.com/user", Scopes: []string{"openid", "profile", "email"}, FieldMapping: &v1pb.FieldMapping{ Identifier: "sub", DisplayName: "given_name", Email: "email", AvatarUrl: "picture", }, }, }, }, }, UpdateMask: &fieldmaskpb.FieldMask{ Paths: []string{"title", "identifier_filter", "config"}, }, } updated, err := ts.Service.UpdateIdentityProvider(userCtx, updateReq) require.NoError(t, err) require.NotNil(t, updated) require.Equal(t, "Updated Provider", updated.Title) require.Equal(t, "test@example.com", updated.IdentifierFilter) require.Equal(t, "updated-client", updated.Config.GetOauth2Config().ClientId) }) t.Run("UpdateIdentityProvider empty ClientSecret preserves existing credential", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, hostUser.ID) // Create IDP with a real secret. created, err := ts.Service.CreateIdentityProvider(userCtx, &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Preserve Secret Test", Type: v1pb.IdentityProvider_OAUTH2, Config: &v1pb.IdentityProviderConfig{ Config: &v1pb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &v1pb.OAuth2Config{ ClientId: "cid", ClientSecret: "original-secret", AuthUrl: "https://ex.com/auth", TokenUrl: "https://ex.com/token", UserInfoUrl: "https://ex.com/user", FieldMapping: &v1pb.FieldMapping{Identifier: "id"}, }, }, }, }, }) require.NoError(t, err) // Update with empty ClientSecret (simulating UI that doesn't resend the secret). _, err = ts.Service.UpdateIdentityProvider(userCtx, &v1pb.UpdateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Name: created.Name, Title: "Updated Title", Type: v1pb.IdentityProvider_OAUTH2, Config: &v1pb.IdentityProviderConfig{ Config: &v1pb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &v1pb.OAuth2Config{ ClientId: "cid", ClientSecret: "", // empty = preserve existing AuthUrl: "https://ex.com/auth", TokenUrl: "https://ex.com/token", UserInfoUrl: "https://ex.com/user", FieldMapping: &v1pb.FieldMapping{Identifier: "id"}, }, }, }, }, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"title", "config"}}, }) require.NoError(t, err) // Verify the stored secret was preserved by reading from store directly. uid, _ := apiv1.ExtractIdentityProviderUIDFromName(created.Name) stored, err := ts.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{UID: &uid}) require.NoError(t, err) require.Equal(t, "original-secret", stored.Config.GetOauth2Config().ClientSecret, "existing ClientSecret must be preserved when an empty value is sent") require.Equal(t, "Updated Title", stored.Name) }) t.Run("UpdateIdentityProvider missing update mask", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) req := &v1pb.UpdateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Name: "identity-providers/1", Title: "Updated Provider", }, } _, err = ts.Service.UpdateIdentityProvider(userCtx, req) require.Error(t, err) require.Contains(t, err.Error(), "update_mask is required") }) t.Run("UpdateIdentityProvider invalid name", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) req := &v1pb.UpdateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Name: "invalid-name", Title: "Updated Provider", }, UpdateMask: &fieldmaskpb.FieldMask{ Paths: []string{"title"}, }, } _, err = ts.Service.UpdateIdentityProvider(userCtx, req) require.Error(t, err) require.Contains(t, err.Error(), "invalid identity provider name") }) } func TestDeleteIdentityProvider(t *testing.T) { ctx := context.Background() t.Run("DeleteIdentityProvider success", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) // Create identity provider createReq := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Provider to Delete", Type: v1pb.IdentityProvider_OAUTH2, Config: &v1pb.IdentityProviderConfig{ Config: &v1pb.IdentityProviderConfig_Oauth2Config{ Oauth2Config: &v1pb.OAuth2Config{ ClientId: "client-to-delete", AuthUrl: "https://example.com/auth", TokenUrl: "https://example.com/token", UserInfoUrl: "https://example.com/user", FieldMapping: &v1pb.FieldMapping{ Identifier: "id", }, }, }, }, }, } created, err := ts.Service.CreateIdentityProvider(userCtx, createReq) require.NoError(t, err) // Delete identity provider deleteReq := &v1pb.DeleteIdentityProviderRequest{ Name: created.Name, } _, err = ts.Service.DeleteIdentityProvider(userCtx, deleteReq) require.NoError(t, err) // Verify deletion getReq := &v1pb.GetIdentityProviderRequest{ Name: created.Name, } _, err = ts.Service.GetIdentityProvider(ctx, getReq) require.Error(t, err) require.Contains(t, err.Error(), "not found") }) t.Run("DeleteIdentityProvider invalid name", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) req := &v1pb.DeleteIdentityProviderRequest{ Name: "invalid-name", } _, err = ts.Service.DeleteIdentityProvider(userCtx, req) require.Error(t, err) require.Contains(t, err.Error(), "invalid identity provider name") }) t.Run("DeleteIdentityProvider not found", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create host user hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, hostUser.ID) req := &v1pb.DeleteIdentityProviderRequest{ Name: "identity-providers/999", } _, err = ts.Service.DeleteIdentityProvider(userCtx, req) require.Error(t, err) // Note: Delete might succeed even if item doesn't exist, depending on store implementation }) } func TestIdentityProviderPermissions(t *testing.T) { ctx := context.Background() t.Run("Only host users can create identity providers", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() // Create regular user regularUser, err := ts.CreateRegularUser(ctx, "regularuser") require.NoError(t, err) // Set user context userCtx := ts.CreateUserContext(ctx, regularUser.ID) req := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Test Provider", Type: v1pb.IdentityProvider_OAUTH2, }, } _, err = ts.Service.CreateIdentityProvider(userCtx, req) require.Error(t, err) require.Contains(t, err.Error(), "permission denied") }) t.Run("Authentication required", func(t *testing.T) { ts := NewTestService(t) defer ts.Cleanup() req := &v1pb.CreateIdentityProviderRequest{ IdentityProvider: &v1pb.IdentityProvider{ Title: "Test Provider", Type: v1pb.IdentityProvider_OAUTH2, }, } _, err := ts.Service.CreateIdentityProvider(ctx, req) require.Error(t, err) require.Contains(t, err.Error(), "user not authenticated") }) }