Mock MS Graph SDK в модульном тесте Golang?

В настоящее время я создаю сервис, позволяющий пользователям предоставлять себе самообслуживаемый доступ к определенным приложениям, при этом доступ к этим приложениям управляется через группы 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%, с чего начать.

Если бы кто-нибудь мог помочь мне указать правильное направление, я был бы очень благодарен!

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
77
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Спасибо за подробный пример.

Давайте сначала посмотрим, что вы используете. 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, чтобы вернуть что-то более удобное - последнее означает, что в нем есть хотя бы некоторая логика, которую необходимо протестировать, тогда как в настоящее время это просто тонкая обертка.

Другие вопросы по теме

Похожие вопросы