From 572c3c72245be4c5d798e755fb15716edce401e7 Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Mon, 16 Oct 2023 16:52:54 -0700 Subject: [PATCH] chore: Mock HttpResponse. --- src/lib/Helpers/ParsingHelpers.cs | 25 +++++---- .../ApiManifestDocumentExtensions.cs | 38 +++++++------- .../ApiManifest.Tests.csproj | 6 ++- .../Helpers/ParsingHelpersTests.cs | 36 ++++++++++--- .../OpenAIPluginManifestTests.cs | 32 +++--------- .../TestFiles/testOpenApi.yaml | 51 +++++++++++++++++++ 6 files changed, 129 insertions(+), 59 deletions(-) create mode 100644 tests/ApiManifest.Tests/TestFiles/testOpenApi.yaml diff --git a/src/lib/Helpers/ParsingHelpers.cs b/src/lib/Helpers/ParsingHelpers.cs index b2b652f..47df3fd 100644 --- a/src/lib/Helpers/ParsingHelpers.cs +++ b/src/lib/Helpers/ParsingHelpers.cs @@ -139,38 +139,43 @@ internal static IEnumerable> ParseKey(string key) } } - internal static async Task ParseOpenApiAsync(string openApiFileUrl, bool inlineExternal, CancellationToken cancellationToken) + internal static async Task ParseOpenApiAsync(Uri openApiFileUri, bool inlineExternal, CancellationToken cancellationToken) + { + Stream stream = await GetStreamAsync(openApiFileUri, cancellationToken: cancellationToken); + return await ParseOpenApiAsync(stream, openApiFileUri, inlineExternal, cancellationToken); + } + + internal static async Task ParseOpenApiAsync(Stream stream, Uri openApiFileUri, bool inlineExternal, CancellationToken cancellationToken) { - Stream stream = await GetStreamAsync(openApiFileUrl, cancellationToken); ReadResult result = await new OpenApiStreamReader(new OpenApiReaderSettings { LoadExternalRefs = inlineExternal, - BaseUrl = new Uri(openApiFileUrl) + BaseUrl = openApiFileUri } ).ReadAsync(stream, cancellationToken); return result; } - private static async Task GetStreamAsync(string input, CancellationToken cancellationToken = default) + internal static async Task GetStreamAsync(Uri uri, HttpMessageHandler? finalHandler = null, CancellationToken cancellationToken = default) { - if (!input.StartsWith("http")) - throw new ArgumentException($"The input {input} is not a valid url", nameof(input)); + if (!uri.Scheme.StartsWith("http")) + throw new ArgumentException($"The input {uri} is not a valid url", nameof(uri)); try { - var httpClientHandler = new HttpClientHandler() + finalHandler ??= new HttpClientHandler() { SslProtocols = System.Security.Authentication.SslProtocols.Tls12, }; - using var httpClient = new HttpClient(httpClientHandler) + using var httpClient = new HttpClient(finalHandler) { DefaultRequestVersion = HttpVersion.Version20 }; - return await httpClient.GetStreamAsync(input, cancellationToken); + return await httpClient.GetStreamAsync(uri, cancellationToken); } catch (HttpRequestException ex) { - throw new InvalidOperationException($"Could not download the file at {input}", ex); + throw new InvalidOperationException($"Could not download the file at {uri}", ex); } } } diff --git a/src/lib/TypeExtensions/ApiManifestDocumentExtensions.cs b/src/lib/TypeExtensions/ApiManifestDocumentExtensions.cs index 900b3e8..7d6ff8e 100644 --- a/src/lib/TypeExtensions/ApiManifestDocumentExtensions.cs +++ b/src/lib/TypeExtensions/ApiManifestDocumentExtensions.cs @@ -9,16 +9,16 @@ namespace Microsoft.OpenApi.ApiManifest.TypeExtensions public static class ApiManifestDocumentExtensions { /// - /// Generates an OpenAIPluginManifest from the provided ApiManifestDocument. + /// Converts an instance of to an instance of . /// /// A valid instance of to generate an OpenAI Plugin manifest from. - /// The URL to a logo for the plugin. + /// The URL to a logo for the plugin. /// The URL to a page with legal information about the plugin. /// The name of apiDependency to use from the provided . The method defaults to the first apiDependency in if no value is provided. - /// The relative path to where the OpenAPI file that's packaged with the plugin manifest if stored. The method default './openapi.json' if none is provided. + /// The path to where the OpenAPI file that's packaged with the plugin manifest is stored. The method defaults to the ApiDependency.ApiDescriptionUrl if none is provided. /// Propagates notification that operations should be canceled. /// A - public static async Task ToOpenAIPluginManifestAsync(this ApiManifestDocument apiManifestDocument, string logoUrl, string legalInfoUrl, string? apiDependencyName = default, string openApiFilePath = "./openapi.json", CancellationToken cancellationToken = default) + public static async Task ToOpenAIPluginManifestAsync(this ApiManifestDocument apiManifestDocument, string logoUrl, string legalInfoUrl, string? apiDependencyName = default, string? openApiPath = default, CancellationToken cancellationToken = default) { if (!TryGetApiDependency(apiManifestDocument.ApiDependencies, apiDependencyName, out ApiDependency? apiDependency)) { @@ -30,26 +30,28 @@ public static async Task ToOpenAIPluginManifestAsync(this } else { - var result = await ParsingHelpers.ParseOpenApiAsync(apiDependency.ApiDescriptionUrl, false, cancellationToken); - return apiManifestDocument.ToOpenAIPluginManifest(openApiDocument: result.OpenApiDocument, logoUrl: logoUrl, legalInfoUrl: legalInfoUrl, openApiFilePath: openApiFilePath); + var result = await ParsingHelpers.ParseOpenApiAsync(new Uri(apiDependency.ApiDescriptionUrl), false, cancellationToken); + return apiManifestDocument.ToOpenAIPluginManifest(result.OpenApiDocument, logoUrl, legalInfoUrl, openApiPath ?? apiDependency.ApiDescriptionUrl); } } - internal static OpenAIPluginManifest ToOpenAIPluginManifest(this ApiManifestDocument apiManifestDocument, OpenApiDocument openApiDocument, string logoUrl, string legalInfoUrl, string openApiFilePath) + /// + /// Converts an instance of to an instance of . + /// + /// A valid instance of with at least one API dependency. + /// The OpenAPI document to use for the OpenAIPluginManifest. + /// The URL to a logo for the plugin. + /// The URL to a page with legal information about the plugin. + /// The path to where the OpenAPI file that's packaged with the plugin manifest is stored. + /// A + public static OpenAIPluginManifest ToOpenAIPluginManifest(this ApiManifestDocument apiManifestDocument, OpenApiDocument openApiDocument, string logoUrl, string legalInfoUrl, string openApiPath) { - // Validate the ApiManifestDocument before generating the OpenAI manifest. + // Validates the ApiManifestDocument before generating the OpenAI manifest. This includes the publisher object. apiManifestDocument.Validate(); - string contactEmail = string.IsNullOrWhiteSpace(apiManifestDocument.Publisher?.ContactEmail) ? string.Empty : apiManifestDocument.Publisher.ContactEmail; - - var openApiManifest = OpenApiPluginFactory.CreateOpenAIPluginManifest( - schemaVersion: openApiDocument.Info.Version, - nameForHuman: openApiDocument.Info.Title, - nameForModel: openApiDocument.Info.Title, - logoUrl: logoUrl, - contactEmail: contactEmail, - legalInfoUrl: legalInfoUrl); + string contactEmail = apiManifestDocument.Publisher?.ContactEmail!; - openApiManifest.Api = new Api("openapi", openApiFilePath); + var openApiManifest = OpenApiPluginFactory.CreateOpenAIPluginManifest(openApiDocument.Info.Title, openApiDocument.Info.Title, logoUrl, contactEmail, legalInfoUrl, openApiDocument.Info.Version); + openApiManifest.Api = new Api("openapi", openApiPath); openApiManifest.Auth = new ManifestNoAuth(); openApiManifest.DescriptionForHuman = openApiDocument.Info.Description ?? $"Description for {openApiManifest.NameForHuman}."; openApiManifest.DescriptionForModel = openApiManifest.DescriptionForHuman; diff --git a/tests/ApiManifest.Tests/ApiManifest.Tests.csproj b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj index a4771c0..f1f534e 100644 --- a/tests/ApiManifest.Tests/ApiManifest.Tests.csproj +++ b/tests/ApiManifest.Tests/ApiManifest.Tests.csproj @@ -18,6 +18,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -37,6 +38,9 @@ Always + + Always + - + \ No newline at end of file diff --git a/tests/ApiManifest.Tests/Helpers/ParsingHelpersTests.cs b/tests/ApiManifest.Tests/Helpers/ParsingHelpersTests.cs index f5666d1..7e7297a 100644 --- a/tests/ApiManifest.Tests/Helpers/ParsingHelpersTests.cs +++ b/tests/ApiManifest.Tests/Helpers/ParsingHelpersTests.cs @@ -2,6 +2,10 @@ // Licensed under the MIT license. using Microsoft.OpenApi.ApiManifest.Helpers; +using Moq; +using Moq.Protected; +using System.Net; +using System.Net.Http.Headers; using System.Text.Json; namespace Microsoft.OpenApi.ApiManifest.Tests.Helpers @@ -116,8 +120,12 @@ public void ParseKeyValuePair() [Fact] public async Task ParseOpenApiAsync() { - var openApiUrl = "https://raw.githubusercontent.com/APIPatterns/Moostodon/main/spec/tsp-output/%40typespec/openapi3/openapi.yaml"; - var results = await ParsingHelpers.ParseOpenApiAsync(openApiUrl, false, CancellationToken.None); + var testOpenApiFilePath = Path.Combine(".", "TestFiles", "testOpenApi.yaml"); + var mockHandler = MockHttpResponse(File.ReadAllText(testOpenApiFilePath)); + + var openApiUri = new Uri("https://contoso.com/openapi.yaml"); + var stream = await ParsingHelpers.GetStreamAsync(openApiUri, mockHandler, CancellationToken.None); + var results = await ParsingHelpers.ParseOpenApiAsync(stream, openApiUri, false, CancellationToken.None); Assert.Empty(results.OpenApiDiagnostic.Errors); Assert.NotNull(results.OpenApiDocument); } @@ -125,15 +133,31 @@ public async Task ParseOpenApiAsync() [Fact] public void ParseOpenApiWithWrongOpenApiUrl() { - var openApiUrl = "https://contoso.com/APIPatterns/Contoso/main/spec/tsp-output/%40typespec/openapi3/openapi.yaml"; - _ = Assert.ThrowsAsync(async () => await ParsingHelpers.ParseOpenApiAsync(openApiUrl, false, CancellationToken.None)); + var openApiUri = new Uri("https://contoso.com/NotValid.yaml"); + _ = Assert.ThrowsAsync(async () => await ParsingHelpers.ParseOpenApiAsync(openApiUri, false, CancellationToken.None)); } [Fact] public void ParseOpenApiWithOpenApiUrlWithAnInvalidSchema() { - var openApiUrl = "contoso.com/APIPatterns/Contoso/main/spec/tsp-output/%40typespec/openapi3/openapi.yaml"; - _ = Assert.ThrowsAsync(async () => await ParsingHelpers.ParseOpenApiAsync(openApiUrl, false, CancellationToken.None)); + var openApiUri = new Uri("xyx://contoso.com/openapi.yaml"); + _ = Assert.ThrowsAsync(async () => await ParsingHelpers.ParseOpenApiAsync(openApiUri, false, CancellationToken.None)); + } + + private static DelegatingHandler MockHttpResponse(string responseContent) + { + var mockResponse = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(responseContent) }; + mockResponse.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + var mockHandler = new Mock(); + _ = mockHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Returns(Task.FromResult(mockResponse)); + return mockHandler.Object; } } } diff --git a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs index 61b95c0..178b622 100644 --- a/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs +++ b/tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs @@ -499,7 +499,7 @@ public void LoadOpenAIPluginManifestWithNoApiUrl() [Fact] public async Task GenerateOpenAIPluginManifestFromApiManifestAsync() { - var openAiPluginManifest = await exampleApiManifest.ToOpenAIPluginManifestAsync(logoUrl: "https://avatars.githubusercontent.com/bar", legalInfoUrl: "https://legalinfo.foobar.com"); + var openAiPluginManifest = await exampleApiManifest.ToOpenAIPluginManifestAsync("https://avatars.githubusercontent.com/bar", "https://legalinfo.foobar.com"); Assert.Equal("1.0.0", openAiPluginManifest.SchemaVersion); Assert.Equal("Mastodon", openAiPluginManifest.NameForHuman); @@ -508,7 +508,7 @@ public async Task GenerateOpenAIPluginManifestFromApiManifestAsync() Assert.Equal("Description for Mastodon.", openAiPluginManifest.DescriptionForModel); _ = Assert.IsType(openAiPluginManifest.Auth); Assert.Equal("openapi", openAiPluginManifest.Api?.Type); - Assert.Equal("./openapi.json", openAiPluginManifest.Api?.Url); + Assert.Equal(exampleApiManifest.ApiDependencies.FirstOrDefault().Value.ApiDescriptionUrl, openAiPluginManifest.Api?.Url); Assert.Equal("https://avatars.githubusercontent.com/bar", openAiPluginManifest.LogoUrl); Assert.Equal("https://legalinfo.foobar.com", openAiPluginManifest.LegalInfoUrl); } @@ -516,11 +516,7 @@ public async Task GenerateOpenAIPluginManifestFromApiManifestAsync() [Fact] public async Task GenerateOpenAIPluginManifestFromApiManifestOfAnApiDependencyAsync() { - var openAiPluginManifest = await exampleApiManifest.ToOpenAIPluginManifestAsync( - logoUrl: "https://avatars.githubusercontent.com/bar", - legalInfoUrl: "https://legalinfo.foobar.com", - apiDependencyName: "MicrosoftGraph", - openApiFilePath: "./openapi.yml"); + var openAiPluginManifest = await exampleApiManifest.ToOpenAIPluginManifestAsync("https://avatars.githubusercontent.com/bar", "https://legalinfo.foobar.com", "MicrosoftGraph", "./openapi.yml"); Assert.Equal("v1.0", openAiPluginManifest.SchemaVersion); Assert.Equal("DirectoryObjects", openAiPluginManifest.NameForHuman); @@ -537,11 +533,8 @@ public async Task GenerateOpenAIPluginManifestFromApiManifestOfAnApiDependencyAs [Fact] public void GenerateOpenAIPluginManifestFromApiManifestWithWrongApiDependency() { - _ = Assert.ThrowsAsync(async () => await exampleApiManifest.ToOpenAIPluginManifestAsync( - logoUrl: "https://avatars.githubusercontent.com/bar", - legalInfoUrl: "https://legalinfo.foobar.com", - apiDependencyName: "ContosoApi", - openApiFilePath: "./openapi.yml")); + _ = Assert.ThrowsAsync( + async () => await exampleApiManifest.ToOpenAIPluginManifestAsync("https://avatars.githubusercontent.com/bar", "https://legalinfo.foobar.com", "ContosoApi", "./openapi.yml")); } [Fact] @@ -550,22 +543,13 @@ public void GenerateOpenAIPluginManifestFromApiManifestWithEmptyApiDependencies( var apiManifest = LoadTestApiManifestDocument(); apiManifest.ApiDependencies.Clear(); - _ = Assert.ThrowsAsync(async () => await apiManifest.ToOpenAIPluginManifestAsync( - logoUrl: "https://avatars.githubusercontent.com/bar", - legalInfoUrl: "https://legalinfo.foobar.com", - apiDependencyName: "MicrosoftGraph", - openApiFilePath: "./openapi.yml")); + _ = Assert.ThrowsAsync( + async () => await apiManifest.ToOpenAIPluginManifestAsync("https://avatars.githubusercontent.com/bar", "https://legalinfo.foobar.com", "MicrosoftGraph", "./openapi.yml")); } private static OpenAIPluginManifest CreateManifestPlugIn() { - var manifest = OpenApiPluginFactory.CreateOpenAIPluginManifest( - schemaVersion: "1.0.0", - nameForHuman: "TestOAuth", - nameForModel: "TestOAuthModel", - logoUrl: "https://avatars.githubusercontent.com/bar", - legalInfoUrl: "https://legalinfo.foobar.com", - contactEmail: "joe@test.com"); + var manifest = OpenApiPluginFactory.CreateOpenAIPluginManifest("TestOAuthModel", "TestOAuth", "https://avatars.githubusercontent.com/bar", "joe@test.com", "https://legalinfo.foobar.com", "1.0.0"); manifest.DescriptionForHuman = "SomeHumanDescription"; manifest.DescriptionForModel = "SomeModelDescription"; manifest.Auth = new ManifestNoAuth(); diff --git a/tests/ApiManifest.Tests/TestFiles/testOpenApi.yaml b/tests/ApiManifest.Tests/TestFiles/testOpenApi.yaml new file mode 100644 index 0000000..2facc4f --- /dev/null +++ b/tests/ApiManifest.Tests/TestFiles/testOpenApi.yaml @@ -0,0 +1,51 @@ +openapi: 3.0.0 +info: + title: Contoso + version: 1.0.0 +tags: [] +paths: + /api/v1/activity: + post: + operationId: contoso_createActivity + parameters: [] + responses: + '200': + description: Success. + content: + application/json: + schema: + $ref: '#/components/schemas/Contoso.Activity' + '401': + description: Access is unauthorized. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Contoso.CreateActivityForm' +components: + schemas: + Contoso.Activity: + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name + Contoso.CreateActivityForm: + type: object + properties: + id: + type: string + name: + type: string + required: + - id + - name +servers: + - url: https://contoso.com/ + description: Server URL. + variables: {}