Skip to content

Commit

Permalink
chore: Mock HttpResponse.
Browse files Browse the repository at this point in the history
  • Loading branch information
peombwa committed Oct 16, 2023
1 parent 2b4c048 commit 572c3c7
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 59 deletions.
25 changes: 15 additions & 10 deletions src/lib/Helpers/ParsingHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,38 +139,43 @@ internal static IEnumerable<KeyValuePair<string, string>> ParseKey(string key)
}
}

internal static async Task<ReadResult> ParseOpenApiAsync(string openApiFileUrl, bool inlineExternal, CancellationToken cancellationToken)
internal static async Task<ReadResult> 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<ReadResult> 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<Stream> GetStreamAsync(string input, CancellationToken cancellationToken = default)
internal static async Task<Stream> 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);
}
}
}
Expand Down
38 changes: 20 additions & 18 deletions src/lib/TypeExtensions/ApiManifestDocumentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ namespace Microsoft.OpenApi.ApiManifest.TypeExtensions
public static class ApiManifestDocumentExtensions
{
/// <summary>
/// Generates an OpenAIPluginManifest from the provided ApiManifestDocument.
/// Converts an instance of <see cref="ApiManifestDocument"/> to an instance of <see cref="OpenAIPluginManifest"/>.
/// </summary>
/// <param name="apiManifestDocument">A valid instance of <see cref="ApiManifestDocument"/> to generate an OpenAI Plugin manifest from.</param>
/// <param name="logoUrl"> The URL to a logo for the plugin.</param>
/// <param name="logoUrl">The URL to a logo for the plugin.</param>
/// <param name="legalInfoUrl">The URL to a page with legal information about the plugin.</param>
/// <param name="apiDependencyName">The name of apiDependency to use from the provided <see cref="ApiManifestDocument.ApiDependencies"/>. The method defaults to the first apiDependency in <see cref="ApiManifestDocument.ApiDependencies"/> if no value is provided.</param>
/// <param name="openApiFilePath">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.</param>
/// <param name="openApiPath">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.</param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns>A <see cref="Task{OpenAIPluginManifest}"/></returns>
public static async Task<OpenAIPluginManifest> ToOpenAIPluginManifestAsync(this ApiManifestDocument apiManifestDocument, string logoUrl, string legalInfoUrl, string? apiDependencyName = default, string openApiFilePath = "./openapi.json", CancellationToken cancellationToken = default)
public static async Task<OpenAIPluginManifest> 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))
{
Expand All @@ -30,26 +30,28 @@ public static async Task<OpenAIPluginManifest> 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)
/// <summary>
/// Converts an instance of <see cref="ApiManifestDocument"/> to an instance of <see cref="OpenAIPluginManifest"/>.
/// </summary>
/// <param name="apiManifestDocument">A valid instance of <see cref="ApiManifestDocument"/> with at least one API dependency.</param>
/// <param name="openApiDocument">The OpenAPI document to use for the OpenAIPluginManifest.</param>
/// <param name="logoUrl">The URL to a logo for the plugin.</param>
/// <param name="legalInfoUrl">The URL to a page with legal information about the plugin.</param>
/// <param name="openApiPath">The path to where the OpenAPI file that's packaged with the plugin manifest is stored.</param>
/// <returns>A <see cref="OpenAIPluginManifest"/></returns>
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;
Expand Down
6 changes: 5 additions & 1 deletion tests/ApiManifest.Tests/ApiManifest.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="xunit" Version="2.5.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand All @@ -37,6 +38,9 @@
<None Update="TestFiles\exampleApiManifest.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestFiles\testOpenApi.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
</Project>
36 changes: 30 additions & 6 deletions tests/ApiManifest.Tests/Helpers/ParsingHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -116,24 +120,44 @@ 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);
}

[Fact]
public void ParseOpenApiWithWrongOpenApiUrl()
{
var openApiUrl = "https://contoso.com/APIPatterns/Contoso/main/spec/tsp-output/%40typespec/openapi3/openapi.yaml";
_ = Assert.ThrowsAsync<InvalidOperationException>(async () => await ParsingHelpers.ParseOpenApiAsync(openApiUrl, false, CancellationToken.None));
var openApiUri = new Uri("https://contoso.com/NotValid.yaml");
_ = Assert.ThrowsAsync<InvalidOperationException>(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<ArgumentException>(async () => await ParsingHelpers.ParseOpenApiAsync(openApiUrl, false, CancellationToken.None));
var openApiUri = new Uri("xyx://contoso.com/openapi.yaml");
_ = Assert.ThrowsAsync<ArgumentException>(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<DelegatingHandler>();
_ = mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(mockResponse));
return mockHandler.Object;
}
}
}
32 changes: 8 additions & 24 deletions tests/ApiManifest.Tests/OpenAIPluginManifestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -508,19 +508,15 @@ public async Task GenerateOpenAIPluginManifestFromApiManifestAsync()
Assert.Equal("Description for Mastodon.", openAiPluginManifest.DescriptionForModel);
_ = Assert.IsType<ManifestNoAuth>(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);
}

[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);
Expand All @@ -537,11 +533,8 @@ public async Task GenerateOpenAIPluginManifestFromApiManifestOfAnApiDependencyAs
[Fact]
public void GenerateOpenAIPluginManifestFromApiManifestWithWrongApiDependency()
{
_ = Assert.ThrowsAsync<ApiManifestException>(async () => await exampleApiManifest.ToOpenAIPluginManifestAsync(
logoUrl: "https://avatars.githubusercontent.com/bar",
legalInfoUrl: "https://legalinfo.foobar.com",
apiDependencyName: "ContosoApi",
openApiFilePath: "./openapi.yml"));
_ = Assert.ThrowsAsync<ApiManifestException>(
async () => await exampleApiManifest.ToOpenAIPluginManifestAsync("https://avatars.githubusercontent.com/bar", "https://legalinfo.foobar.com", "ContosoApi", "./openapi.yml"));
}

[Fact]
Expand All @@ -550,22 +543,13 @@ public void GenerateOpenAIPluginManifestFromApiManifestWithEmptyApiDependencies(
var apiManifest = LoadTestApiManifestDocument();
apiManifest.ApiDependencies.Clear();

_ = Assert.ThrowsAsync<ApiManifestException>(async () => await apiManifest.ToOpenAIPluginManifestAsync(
logoUrl: "https://avatars.githubusercontent.com/bar",
legalInfoUrl: "https://legalinfo.foobar.com",
apiDependencyName: "MicrosoftGraph",
openApiFilePath: "./openapi.yml"));
_ = Assert.ThrowsAsync<ApiManifestException>(
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: "[email protected]");
var manifest = OpenApiPluginFactory.CreateOpenAIPluginManifest("TestOAuthModel", "TestOAuth", "https://avatars.githubusercontent.com/bar", "[email protected]", "https://legalinfo.foobar.com", "1.0.0");
manifest.DescriptionForHuman = "SomeHumanDescription";
manifest.DescriptionForModel = "SomeModelDescription";
manifest.Auth = new ManifestNoAuth();
Expand Down
51 changes: 51 additions & 0 deletions tests/ApiManifest.Tests/TestFiles/testOpenApi.yaml
Original file line number Diff line number Diff line change
@@ -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: {}

0 comments on commit 572c3c7

Please sign in to comment.