В настоящее время я создаю сервис, позволяющий пользователям предоставлять себе самообслуживаемый доступ к определенным приложениям, при этом доступ к этим приложениям управляется через группы Entra.
У меня есть следующий код golang, позволяющий проверить, является ли пользователь членом указанной группы (это моя первая настоящая работа с Golang и связанным с ним Graph SDK, поэтому извините, если это отстой!)
package entra
import (
"context"
azidentity "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors"
"github.com/spf13/viper"
"go.uber.org/zap"
)
type Credentialer interface {
NewClientSecretCredential(tenantID string, clientID string, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (*azidentity.ClientSecretCredential, error)
}
type GraphClientCreator interface {
NewGraphServiceClientWithCredentials(cred *azidentity.ClientSecretCredential, scopes []string) (*msgraphsdk.GraphServiceClient, error)
}
type Service struct {
Credentialer Credentialer
GraphClientCreator GraphClientCreator
}
type AzureCredentialer struct{}
func (ac *AzureCredentialer) NewClientSecretCredential(tenantID string, clientID string, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (*azidentity.ClientSecretCredential, error) {
return azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, options)
}
type MsGraphClientCreator struct{}
func (mgcc *MsGraphClientCreator) NewGraphServiceClientWithCredentials(cred *azidentity.ClientSecretCredential, scopes []string) (*msgraphsdk.GraphServiceClient, error) {
return msgraphsdk.NewGraphServiceClientWithCredentials(cred, scopes)
}
func (s *Service) GetGraphClient() (*msgraphsdk.GraphServiceClient, error) {
// Get the Azure AD client ID
clientId := viper.GetString("client_id")
tenantId := viper.GetString("tenant_id")
clientSecret := viper.GetString("client_secret")
// Create creds
clientCredentials, err := s.Credentialer.NewClientSecretCredential(tenantId, clientId, clientSecret, nil)
if err != nil {
printOdataError(err)
zap.S().Error("Error creating managed identity credentials: ", err)
return nil, err
}
zap.S().Debug("Managed identity credentials created successfully")
// Create a new Graph client
graphClient, err := s.GraphClientCreator.NewGraphServiceClientWithCredentials(
clientCredentials,
[]string{"https://graph.microsoft.com/.default"})
if err != nil {
printOdataError(err)
zap.S().Error("Error creating graph client: ", err)
return nil, err
}
zap.S().Debug("Graph client created successfully")
return graphClient, nil
}
func IsUserInGroup(groupId string, userId string) (bool, error) {
service := &Service{
Credentialer: &AzureCredentialer{},
GraphClientCreator: &MsGraphClientCreator{},
}
// Get the graph client
graphClient, err := service.GetGraphClient()
if err != nil {
printOdataError(err)
zap.S().Error("Error getting graph client: ", err)
return false, err
}
zap.S().Debug("Getting group members...")
zap.S().Debug("Group ID: ", groupId)
zap.S().Debug("User ID: ", userId)
group, err := graphClient.Users().ByUserId(userId).MemberOf().Get(context.Background(), nil)
if err != nil {
printOdataError(err)
zap.S().Error("Error getting group members: ", err)
return false, err
}
zap.S().Debug("Group memberships: ", len(group.GetValue()))
for _, membership := range group.GetValue() {
if *membership.GetId() == groupId {
zap.S().Debug("User is a member of the group")
return true, nil
}
}
return false, nil
}
Однако, когда дело доходит до попытки модульного тестирования, я не могу понять, как соответствующим образом имитировать ответы Graph, и я не уверен на 100%, с чего начать.
Если бы кто-нибудь мог помочь мне указать правильное направление, я был бы очень благодарен!





Спасибо за подробный пример.
Давайте сначала посмотрим, что вы используете. AFAICS единственная точка, где вы звоните *msgraphsdk.GraphServiceClient и используете ответ, это
group, err := graphClient.Users().ByUserId(userId).MemberOf().Get(context.Background(), nil)
/// [...]
for _, membership := range group.GetValue() {
/// [...]
}
Итак, давайте посмеяться над этим. Поскольку мы хотим протестировать нашу бизнес-логику (состоит пользователь в группе или нет), пишем короткую обертку для вызова:
type MSGraphClient struct {
c *msgraphsdk.GraphServiceClient
}
func (g MSGraphClient) UserGroupsByUserID(
ctx context.Context, userID string,
) ([]models.DirectoryObjectable, error) {
response, err := g.c.Users().ByUserId(userID).MemberOf().Get(ctx, nil)
if err != nil {
return nil, err
}
return response.GetValue(), nil
}
Это преобразует наш код выше в
gc := MSGraphClient{c: graphClient}
groups, err := gc.UserGroupsByUserID(context.Background(), userId)
// [...]
for _, membership := range groups {
// [...]
}
Теперь, когда мы упростили ситуацию, давайте посмотрим, от чего зависит наш код:
type GraphClient interface {
UserGroupsByUserID(ctx context.Context, userID string) ([]models.DirectoryObjectable, error)
}
Большой. Давайте реорганизуем наш IsUserInGroup, чтобы его можно было тестировать, и внедрим соответствующих соавторов:
func IsUserInGroup(ctx context.Context, graphClient GraphClient, groupID string, userID string) (bool, error) {
groups, err := graphClient.UserGroupsByUserID(ctx, userID)
if err != nil {
return false, fmt.Errorf("error getting groups for user %s: %w", userID, err)
}
for _, membership := range groups {
if *membership.GetId() == groupID {
return true, nil
}
}
return false, nil
}
Теперь это можно проверить, написав свой собственный TestGraphClient:
type TestGraphClient struct{}
func (TestGraphClient) UserGroupsByUserID(_ context.Context, userID string) ([]models.DirectoryObjectable, error) {
if userID != "testUser" {
err := odataerrors.NewODataError()
return nil, err
}
var result []models.DirectoryObjectable
result = append(result, TestDirectoryObjectable{id: "group1"})
return result, nil
}
и используя TestDirectoryObjectable типа:
type TestDirectoryObjectable struct{ id string }
func (t TestDirectoryObjectable) GetId() *string {
return &t.id
}
// [...]
func (TestDirectoryObjectable) OtherFuncs() {
panic("unimplemented")
}
Ваша IDE должна помочь с реализацией недостающих функций models.DirectoryObjectable.
Если это слишком много шаблонного кода, вы можете либо позволить издевательству написать код за вас, либо адаптировать MSGraphClient, чтобы вернуть что-то более удобное - последнее означает, что в нем есть хотя бы некоторая логика, которую необходимо протестировать, тогда как в настоящее время это просто тонкая обертка.