Skip to content

[OpenAPI] Add endpoint for create new admin resource details #307 #313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 16, 2024

Conversation

o-ii
Copy link
Contributor

@o-ii o-ii commented Sep 10, 2024

Issue

#307 (참조 PR #261, #231)

[OpenAPI] Add endpoint for create new admin resource details

  • OpenAPI doc only - no implementation
  • POST
  • /admin/resources

Summary of Changes

1. src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs

- 새로운 엔드포인트 URL 추가

2. src/AzureOpenAIProxy.ApiApp/Endpoints/AdminResourceEndpoints.cs

- `AddNewAdminResource` 메소드 구현

3. src/AzureOpenAIProxy.ApiApp/Program.cs

- `app.AddNewAdminResource();` 메소드 호출 추가

4. test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs

- 테스트 코드 작성

질문

  • 현재 admin 태그에 events와 resources가 함께 분류되어 있습니다.
    • 태그 설명을 "Admin for organizing events and resources"로 수정하는 것이 좋을까요,
    • 아니면 events와 resources를 각각 별도의 태그로 분류하는 것이 더 명확할까요?
Screenshot 2024-09-10 at 9 17 15 PM
// src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs

swaggerDoc.Tags =
[
    new OpenApiTag { Name = "weather", Description = "Weather forecast operations" },
    new OpenApiTag { Name = "openai", Description = "Azure OpenAI operations" },
    new OpenApiTag { Name = "admin", Description = "Admin for organizing events" },
    new OpenApiTag { Name = "events", Description = "User events" }
];

Copy link
Contributor

@justinyoo justinyoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 admin 태그로 다 넣죠. 따로 resource 태그를 만들 필요는 없을 것 같아요.

@o-ii
Copy link
Contributor Author

o-ii commented Sep 14, 2024

일단 admin 태그로 다 넣죠. 따로 resource 태그를 만들 필요는 없을 것 같아요.

resource 태그는 따로 만들지 않고,
admin 태그의 Description을 "Admin operations for managing events and resources"로 수정했습니다.

여기에 AdminResourceDetails에 대한 테스트를 추가해 주세요.

AdminResourceDetails에 대한 4가지 테스트를 추가했습니다.

  1. required: 각 속성의 필수 여부 검증 (필수 속성은 required 배열에 포함되어 있고, 선택적 속성은 포함되어 있지 않은지 확인)
  2. property: 각 속성이 OpenAPI 스펙에 정의되어 있는지 확인
  3. type: 각 속성의 타입이 올바르게 정의되어 있는지 확인
  4. enum: resourceType 속성이 Enum으로 정의되어 있고, 올바른 값들을 포함하는지 확인

@o-ii
Copy link
Contributor Author

o-ii commented Sep 14, 2024

[질문]
테스트 코드에서 사용되지 않는 파라미터 경고를 해결하는 두 가지 방식 중 어느 쪽이 더 나을까요?

1. 기존 방식 (#231)

사용되지 않는 파라미터를 var로 할당하여 경고를 피하는 방식

[Theory]
[MemberData(nameof(AttributeData))]
public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Type(string attribute, bool isRequired, string type)
{
    // Arrange
    using var httpClient = host.App!.CreateHttpClient("apiapp");

    // 경고 방지를 위한 임시 할당 (실제로 사용되지 않음)
    var isReq = isRequired;
    var typeStr = type;

    // Act & Assert 생략
}

2. Discard 패턴 사용

_1, _2와 같은 discard 패턴을 사용하여 사용되지 않는 파라미터를 명확히 무시하는 방식

[Theory]
[MemberData(nameof(AttributeData))]
public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Type(string attribute, bool _1, string _2)
{
    // Arrange
    using var httpClient = host.App!.CreateHttpClient("apiapp");

    // Act & Assert 생략
}

@o-ii o-ii requested a review from justinyoo September 14, 2024 05:35
@justinyoo
Copy link
Contributor

테스트 코드에서 사용되지 않는 파라미터 경고를 해결하는 두 가지 방식 중 어느 쪽이 더 나을까요?

읭? 쓰이지 않는 파라미터 혹은 변수를 굳이 정의하는 이유가 있나요? 거기서부터 시작해야 할텐데?

@o-ii
Copy link
Contributor Author

o-ii commented Sep 14, 2024

테스트 코드에서 사용되지 않는 파라미터 경고를 해결하는 두 가지 방식 중 어느 쪽이 더 나을까요?

읭? 쓰이지 않는 파라미터 혹은 변수를 굳이 정의하는 이유가 있나요? 거기서부터 시작해야 할텐데?

여러 테스트에서 동일한 AttributeData를 참조하고 있는데, 테스트 종류에 따라 필요한 파라미터가 다르다 보니, 사용되지 않는 파라미터도 함께 전달되고 있습니다.

public static IEnumerable<object[]> AttributeData =>
    [
        ["resourceId", true, "string"],
        ["friendlyName", true, "string"],
        ["deploymentName", true, "string"],
        ["resourceType", true, "string"],
        ["endpoint", true, "string"],
        ["apiKey", true, "string"],
        ["region", true, "string"],
        ["isActive", true, "boolean"]
    ];

기존 테스트 코드 스타일을 유지하기 위해 [MemberData(nameof(AttributeData))]를 활용했는데, [InlineData]를 사용하는 것이 더 나을까요?

@justinyoo
Copy link
Contributor

여러 테스트에서 동일한 AttributeData를 참조하고 있는데, 테스트 종류에 따라 사용되는 파라미터가 다르다 보니, 사용되지 않는 파라미터도 함께 전달되고 있습니다.

기존 테스트 코드 스타일을 유지하기 위해 [MemberData(nameof(AttributeData))]를 사용했는데, [InlineData]를 사용하는 것이 더 나을까요?

해당 개체의 구조가 내 테스트에 맞지 않다면 다른 형태로 테스트를 작성하는 게 맞다고 생각해요.

@o-ii
Copy link
Contributor Author

o-ii commented Sep 14, 2024

해당 개체의 구조가 내 테스트에 맞지 않다면 다른 형태로 테스트를 작성하는 게 맞다고 생각해요.

네, 현재 제가 테스트하려는 모델의 구조가 test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminGetEventDetailsOpenApiTests.cs(PR #231)와 비슷해서 이 코드를 참고했습니다.

    [Theory]
    [MemberData(nameof(AttributeData))]
    public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Property(string attribute, bool isRequired, string type)
    {
        // Arrange
        using var httpClient = host.App!.CreateHttpClient("apiapp");
        await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));

        var isReq = isRequired;
        var typeStr = type;

        // Act
        var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json");
        var openapi = JsonSerializer.Deserialize<JsonDocument>(json);

        // Assert
        var result = openapi!.RootElement.GetProperty("components")
                                         .GetProperty("schemas")
                                         .GetProperty("AdminEventDetails")
                                         .GetProperty("properties")
                                         .TryGetProperty(attribute, out var property) ? property : default;
        result.ValueKind.Should().Be(JsonValueKind.Object);
    }

여기서 var isReq = isRequired;, var typeStr = type;가 선언되었지만 실제로 사용되지 않습니다. 이는 컴파일러에서 발생하는 '사용하지 않는 변수' 경고를 해결하기 위해 추가된 것으로 보입니다.

심각한 오류는 아니고, 경고 수준의 메시지가 떴습니다.

Theory method 'Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Property' on test class 'AdminCreateResourcesOpenApiTests' does not use parameter '_isRequired'. Use the parameter, or remove the parameter and associated data.

[InlineData] 방식을 사용하면 각 테스트에 필요한 파라미터만 전달할 수 있지만, 여러 테스트에 중복되는 입력을 작성해야 하는 단점이 있어서, 기존 테스트 코드와 일관되게[MemberData(nameof(AttributeData))] 방식을 사용했습니다.

@justinyoo
Copy link
Contributor

[InlineData] 방식을 사용하면 각 테스트에 필요한 파라미터만 전달할 수 있지만, 여러 테스트에 중복되는 입력을 작성해야 하는 단점이 있어서, 기존 테스트 코드와 일관되게[MemberData(nameof(AttributeData))] 방식을 사용했습니다.

그러면 굳이 기존의 AttributeData 대신 테스트에 필요한 개체를 따로 만들고 그걸 보내는 게 낫지 않을까요? 그게 InlineData가 됐든 MemberData가 됐든 그건 문제가 아닐 것 같아요.

@o-ii
Copy link
Contributor Author

o-ii commented Sep 14, 2024

그러면 굳이 기존의 AttributeData 대신 테스트에 필요한 개체를 따로 만들고 그걸 보내는 게 낫지 않을까요? 그게 InlineData가 됐든 MemberData가 됐든 그건 문제가 아닐 것 같아요.

기존의 AttributeData 대신 각 테스트에 필요한 개체를 별도로 생성하여 사용하는 방식으로 수정했습니다. (1번 코드)
이대로 수정해도 될까요?

1. 각 테스트에 필요한 개체 따로 생성

string attribute, bool isRequired, string type의 조합에 따라 각 테스트에 필요한 데이터만 전달할 수 있게 되어, 불필요한 파라미터를 정의할 필요가 없습니다.

public static IEnumerable<object[]> PropertyAttributeData => 
    [
        ["resourceId"],
        ["friendlyName"],
        ["deploymentName"],
        ["resourceType"],
        ["endpoint"],
        ["apiKey"],
        ["region"],
        ["isActive"]
    ];

[Theory]
[MemberData(nameof(PropertyAttributeData))]
public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Property(string attribute)
{
    // Act
    var openapi = await GetOpenApiJsonAsync();

    // Assert
    var property = openapi.RootElement.GetProperty("components")
                                      .GetProperty("schemas")
                                      .GetProperty("AdminResourceDetails")
                                      .GetProperty("properties")
                                      .GetProperty(attribute);

    property.ValueKind.Should().Be(JsonValueKind.Object);
}

public static IEnumerable<object[]> TypeAttributeData => 
    [
        ["resourceId", "string"],
        ["friendlyName", "string"],
        ["deploymentName", "string"],
        ["resourceType", "string"],
        ["endpoint", "string"],
        ["apiKey", "string"],
        ["region", "string"],
        ["isActive", "boolean"]
    ];

[Theory]
[MemberData(nameof(TypeAttributeData))]
public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Type(string attribute, string expectedType)
{
    // Act
    var openapi = await GetOpenApiJsonAsync();

    // Assert
    var property = openapi.RootElement.GetProperty("components")
                                      .GetProperty("schemas")
                                      .GetProperty("AdminResourceDetails")
                                      .GetProperty("properties")
                                      .GetProperty(attribute);

    if (property.TryGetProperty("type", out var typeProperty))
    {
        typeProperty.GetString().Should().Be(expectedType);
    }
    else if (property.TryGetProperty("$ref", out var refElement))
    {
        var refPath = refElement.GetString().TrimStart('#', '/').Split('/');
        var refSchema = openapi.RootElement;
        foreach (var part in refPath)
        {
            refSchema = refSchema.GetProperty(part);
        }
        var typeString = refSchema.GetProperty("type").GetString();
        typeString.Should().Be(expectedType);
    }
    else
    {
        throw new Exception($"Type information not found for attribute '{attribute}'");
    }
}

public static IEnumerable<object[]> RequiredAttributeData => 
    [
        ["resourceId", true],
        ["friendlyName", true],
        ["deploymentName", true],
        ["resourceType", true],
        ["endpoint", true],
        ["apiKey", true],
        ["region", true],
        ["isActive", true]
    ];

[Theory]
[MemberData(nameof(RequiredAttributeData))]
public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Required(string attribute, bool isRequired)
{
    // Act
    var openapi = await GetOpenApiJsonAsync();

    // Assert
    var requiredAttributes = openapi.RootElement.GetProperty("components")
                                                .GetProperty("schemas")
                                                .GetProperty("AdminResourceDetails")
                                                .GetProperty("required")
                                                .EnumerateArray()
                                                .Select(p => p.GetString())
                                                .ToList();

    requiredAttributes.Contains(attribute).Should().Be(isRequired);
}

2. 한 개체를 여러 테스트가 함께 공유(불필요한 파라미터는 _ 처리) -> 현재 코드

    public static IEnumerable<object[]> AttributeData =>
        [
            ["resourceId", true, "string"],
            ["friendlyName", true, "string"],
            ["deploymentName", true, "string"],
            ["resourceType", true, "string"],
            ["endpoint", true, "string"],
            ["apiKey", true, "string"],
            ["region", true, "string"],
            ["isActive", true, "boolean"]
        ];

    [Theory]
    [MemberData(nameof(AttributeData))]
    public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Required(string attribute, bool isRequired, string _)
    {
        // Arrange
        using var httpClient = host.App!.CreateHttpClient("apiapp");
        await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));

        // Act
        var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json");
        var openapi = JsonSerializer.Deserialize<JsonDocument>(json);

        // Assert
        var result = openapi!.RootElement.GetProperty("components")
                                         .GetProperty("schemas")
                                         .GetProperty("AdminResourceDetails")
                                         .TryGetStringArray("required")
                                         .ToList();
        result.Contains(attribute).Should().Be(isRequired);
    }

    [Theory]
    [MemberData(nameof(AttributeData))]
    public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Property(string attribute, bool _1, string _2)
    {
        // Arrange
        using var httpClient = host.App!.CreateHttpClient("apiapp");
        await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));

        // Act
        var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json");
        var openapi = JsonSerializer.Deserialize<JsonDocument>(json);

        // Assert
        var result = openapi!.RootElement.GetProperty("components")
                                         .GetProperty("schemas")
                                         .GetProperty("AdminResourceDetails")
                                         .GetProperty("properties")
                                         .GetProperty(attribute);
        result.ValueKind.Should().Be(JsonValueKind.Object);
    }

    [Theory]
    [MemberData(nameof(AttributeData))]
    public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Type(string attribute, bool _, string type)
    {
        // Arrange
        using var httpClient = host.App!.CreateHttpClient("apiapp");
        await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));

        // Act
        var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json");
        var openapi = JsonSerializer.Deserialize<JsonDocument>(json);

        // Assert
        var result = openapi!.RootElement.GetProperty("components")
                                         .GetProperty("schemas")
                                         .GetProperty("AdminResourceDetails")
                                         .GetProperty("properties")
                                         .GetProperty(attribute);

        if (!result.TryGetProperty("type", out var typeProperty))
        {
            var refPath = result.TryGetString("$ref").TrimStart('#', '/').Split('/');
            var refSchema = openapi.RootElement;

            foreach (var part in refPath)
            {
                refSchema = refSchema.GetProperty(part);
            }

            typeProperty = refSchema.GetProperty("type");
        }
        
        typeProperty.GetString().Should().Be(type);
    }

3. [InlineData] 방식으로 구현

    [Theory]
    [InlineData("resourceId")]
    [InlineData("friendlyName")]
    [InlineData("deploymentName")]
    [InlineData("resourceType")]
    [InlineData("endpoint")]
    [InlineData("apiKey")]
    [InlineData("region")]
    [InlineData("isActive")]
    public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Property(string attribute)
    {
        // Act
        var openapi = await GetOpenApiJsonAsync();

        // Assert
        var property = openapi.RootElement.GetProperty("components")
                                          .GetProperty("schemas")
                                          .GetProperty("AdminResourceDetails")
                                          .GetProperty("properties")
                                          .GetProperty(attribute);

        property.ValueKind.Should().Be(JsonValueKind.Object);
    }

    [Theory]
    [InlineData("resourceId", "string")]
    [InlineData("friendlyName", "string")]
    [InlineData("deploymentName", "string")]
    [InlineData("resourceType", "string")]
    [InlineData("endpoint", "string")]
    [InlineData("apiKey", "string")]
    [InlineData("region", "string")]
    [InlineData("isActive", "boolean")]
    public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Type(string attribute, string expectedType)
    {
        // Act
        var openapi = await GetOpenApiJsonAsync();

        // Assert
        var property = openapi.RootElement.GetProperty("components")
                                          .GetProperty("schemas")
                                          .GetProperty("AdminResourceDetails")
                                          .GetProperty("properties")
                                          .GetProperty(attribute);

        if (property.TryGetProperty("type", out var typeProperty))
        {
            typeProperty.GetString().Should().Be(expectedType);
        }
        else if (property.TryGetProperty("$ref", out var refElement))
        {
            var refPath = refElement.GetString().TrimStart('#', '/').Split('/');
            var refSchema = openapi.RootElement;
            foreach (var part in refPath)
            {
                refSchema = refSchema.GetProperty(part);
            }
            var typeString = refSchema.GetProperty("type").GetString();
            typeString.Should().Be(expectedType);
        }
        else
        {
            throw new Exception($"Type information not found for attribute '{attribute}'");
        }
    }

    [Theory]
    [InlineData("resourceId", true)]
    [InlineData("friendlyName", true)]
    [InlineData("deploymentName", true)]
    [InlineData("resourceType", true)]
    [InlineData("endpoint", true)]
    [InlineData("apiKey", true)]
    [InlineData("region", true)]
    [InlineData("isActive", true)]
    public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Required(string attribute, bool isRequired)
    {
        // Act
        var openapi = await GetOpenApiJsonAsync();

        // Assert
        var requiredAttributes = openapi.RootElement.GetProperty("components")
                                                    .GetProperty("schemas")
                                                    .GetProperty("AdminResourceDetails")
                                                    .GetProperty("required")
                                                    .EnumerateArray()
                                                    .Select(p => p.GetString())
                                                    .ToList();

        requiredAttributes.Contains(attribute).Should().Be(isRequired);
    }
}

@justinyoo
Copy link
Contributor

제가 가장 선호하는 방식은 한눈에 바로 들어오는 3번이긴 한데, 1번처럼 해도 괜찮을 듯 합니다.

@o-ii
Copy link
Contributor Author

o-ii commented Sep 14, 2024

제가 가장 선호하는 방식은 한눈에 바로 들어오는 3번이긴 한데, 1번처럼 해도 괜찮을 듯 합니다.

저도 여러 개체를 생성하는 1번 방식보다는 3번 방식처럼 [InlineData]를 사용하는 것이 더 깔끔하다고 생각해서, [MemberData]를 [InlineData]로 변경하고 불필요한 파라미터도 제거했습니다.

현재 수정된 코드는 같은 폴더 내 다른 테스트 코드와 스타일이 다릅니다. 이 파일도 같은 방식으로 수정해야 할까요?

@justinyoo
Copy link
Contributor

현재 수정된 코드는 같은 폴더 내 다른 테스트 코드와 스타일이 다릅니다. 이 파일도 같은 방식으로 수정해야 할까요?

우선 그쪽은 따로 PR을 날려보는 것이 낫겠습니다. PR 넣으면서 #207 팔로업 PR 이라고 명시해 주시면 될 듯 해요.

Copy link
Contributor

@justinyoo justinyoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다! LGTM!

@justinyoo justinyoo merged commit 5656d99 into aliencube:main Sep 16, 2024
1 check passed
@o-ii o-ii deleted the feature/307-endpoint-admin-resources branch September 21, 2024 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[OpenAPI] Add endpoint for create new admin resource details
2 participants