Skip to content

Add API approval tests #557

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions GraphQL.Client.sln
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{98D4
.github\dependabot.yml = .github\dependabot.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.ApiTests", "tests\GraphQL.Client.ApiTests\GraphQL.Client.ApiTests.csproj", "{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -129,6 +131,10 @@ Global
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.Build.0 = Release|Any CPU
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -150,6 +156,7 @@ Global
{7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD} = {89AD33AB-64F6-4F82-822F-21DF7A10CEC0}
{98D4DDDD-DE15-4997-B888-9BC806C7416C} = {63F75859-4698-4EDE-8B70-4ACBB8BC425A}
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4}
Expand Down
10 changes: 0 additions & 10 deletions src/GraphQL.Client/GraphQLHttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ public GraphQLHttpRequest(GraphQLRequest other)
{
}

/// <summary>
/// Allows to preprocess a <see cref="HttpRequestMessage"/> before it is sent, i.e. add custom headers
/// </summary>
[IgnoreDataMember]
[Obsolete("Inherit from GraphQLHttpRequest and override ToHttpRequestMessage() to customize the HttpRequestMessage. Will be removed in v4.0.0.")]
public Action<HttpRequestMessage> PreprocessHttpRequestMessage { get; set; } = message => { };

/// <summary>
/// Creates a <see cref="HttpRequestMessage"/> from this <see cref="GraphQLHttpRequest"/>.
/// Used by <see cref="GraphQLHttpClient"/> to convert GraphQL requests when sending them as regular HTTP requests.
Expand All @@ -48,9 +41,6 @@ public virtual HttpRequestMessage ToHttpRequestMessage(GraphQLHttpClientOptions
if (options.DefaultUserAgentRequestHeader != null)
message.Headers.UserAgent.Add(options.DefaultUserAgentRequestHeader);

#pragma warning disable CS0618 // Type or member is obsolete
PreprocessHttpRequestMessage(message);
#pragma warning restore CS0618 // Type or member is obsolete
return message;
}
}
89 changes: 89 additions & 0 deletions tests/GraphQL.Client.ApiTests/ApiApprovalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Diagnostics;
using System.Reflection;
using System.Xml.Linq;
using GraphQL.Client.Abstractions;
using GraphQL.Client.Abstractions.Websocket;
using GraphQL.Client.Http;
using GraphQL.Client.LocalExecution;
using GraphQL.Client.Serializer.Newtonsoft;
using GraphQL.Client.Serializer.SystemTextJson;
using PublicApiGenerator;
using Shouldly;
using Xunit;

namespace GraphQL.ApiTests;

public class ApiApprovalTests
{
[Theory]
[InlineData(typeof(NewtonsoftJsonSerializer))]
[InlineData(typeof(SystemTextJsonSerializer))]
[InlineData(typeof(GraphQLRequest))]
[InlineData(typeof(GraphQLLocalExecutionClient))]
[InlineData(typeof(IGraphQLWebSocketClient))]
[InlineData(typeof(IGraphQLClient))]
[InlineData(typeof(GraphQLHttpRequest))]
public void PublicApi(Type type)
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
string projectName = type.Assembly.GetName().Name!;
string testDir = Path.Combine(baseDir, $"..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..");
string projectDir = Path.Combine(testDir, "..");
string srcDir = Path.Combine(projectDir, "..", "src");
string buildDir = Path.Combine(srcDir, projectName, "bin", Environment.GetEnvironmentVariable("CI") == null ? "Debug" : "Release");
Debug.Assert(Directory.Exists(buildDir), $"Directory '{buildDir}' doesn't exist");
string csProject = Path.Combine(srcDir, projectName, projectName + ".csproj");
var project = XDocument.Load(csProject);
string[] tfms = project.Descendants("TargetFrameworks").Union(project.Descendants("TargetFramework")).First().Value.Split(";", StringSplitOptions.RemoveEmptyEntries);

// There may be old stuff from earlier builds like net45, netcoreapp3.0, etc. so filter it out
string[] actualTfmDirs = Directory.GetDirectories(buildDir).Where(dir => tfms.Any(tfm => dir.EndsWith(tfm))).ToArray();
Debug.Assert(actualTfmDirs.Length > 0, $"Directory '{buildDir}' doesn't contain subdirectories matching {string.Join(";", tfms)}");

(string tfm, string content)[] publicApi = actualTfmDirs.Select(tfmDir => (new DirectoryInfo(tfmDir).Name.Replace(".", ""), Assembly.LoadFile(Path.Combine(tfmDir, projectName + ".dll")).GeneratePublicApi(new ApiGeneratorOptions
{
IncludeAssemblyAttributes = false,
//AllowNamespacePrefixes = new[] { "Microsoft.Extensions.DependencyInjection" },
ExcludeAttributes = new[] { "System.Diagnostics.DebuggerDisplayAttribute", "System.Diagnostics.CodeAnalysis.AllowNullAttribute" }
}) + Environment.NewLine)).ToArray();

if (publicApi.DistinctBy(item => item.content).Count() == 1)
{
AutoApproveOrFail(publicApi[0].content, "");
}
else
{
foreach (var item in publicApi.ToLookup(item => item.content))
{
AutoApproveOrFail(item.Key, string.Join("+", item.Select(x => x.tfm).OrderBy(x => x)));
}
}

// Approval test should (re)generate approved.txt files locally if needed.
// Approval test should fail on CI.
// https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
void AutoApproveOrFail(string publicApi, string folder)
{
string file = null!;

try
{
publicApi.ShouldMatchApproved(options => options.SubFolder(folder).NoDiff().WithFilenameGenerator((testMethodInfo, discriminator, fileType, fileExtension) => file = $"{type.Assembly.GetName().Name}.{fileType}.{fileExtension}"));
}
catch (ShouldMatchApprovedException) when (Environment.GetEnvironmentVariable("CI") == null)
{
string? received = Path.Combine(testDir, folder, file);
string? approved = received.Replace(".received.txt", ".approved.txt");
if (File.Exists(received) && File.Exists(approved))
{
File.Copy(received, approved, overwrite: true);
File.Delete(received);
}
else
{
throw;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace GraphQL.Client.Abstractions.Websocket
{
public static class GraphQLWebSocketMessageType
{
public const string GQL_COMPLETE = "complete";
public const string GQL_CONNECTION_ACK = "connection_ack";
public const string GQL_CONNECTION_ERROR = "connection_error";
public const string GQL_CONNECTION_INIT = "connection_init";
public const string GQL_CONNECTION_KEEP_ALIVE = "ka";
public const string GQL_CONNECTION_TERMINATE = "connection_terminate";
public const string GQL_DATA = "data";
public const string GQL_ERROR = "error";
public const string GQL_NEXT = "next";
public const string GQL_PING = "ping";
public const string GQL_PONG = "pong";
public const string GQL_START = "start";
public const string GQL_STOP = "stop";
public const string GQL_SUBSCRIBE = "subscribe";
}
public class GraphQLWebSocketRequest : System.Collections.Generic.Dictionary<string, object>, System.IEquatable<GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest>
{
public const string ID_KEY = "id";
public const string PAYLOAD_KEY = "payload";
public const string TYPE_KEY = "type";
public GraphQLWebSocketRequest() { }
public string Id { get; set; }
public object? Payload { get; set; }
public string Type { get; set; }
public bool Equals(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest other) { }
public override bool Equals(object obj) { }
public override int GetHashCode() { }
public void SendCanceled() { }
public void SendCompleted() { }
public void SendFailed(System.Exception e) { }
public System.Threading.Tasks.Task SendTask() { }
public static bool operator !=(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request1, GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request2) { }
public static bool operator ==(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request1, GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request2) { }
}
public class GraphQLWebSocketResponse : System.IEquatable<GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse>
{
public GraphQLWebSocketResponse() { }
public string Id { get; set; }
public string Type { get; set; }
public bool Equals(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse other) { }
public override bool Equals(object obj) { }
public override int GetHashCode() { }
public static bool operator !=(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse response1, GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse response2) { }
public static bool operator ==(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse response1, GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse response2) { }
}
public class GraphQLWebSocketResponse<TPayload> : GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse, System.IEquatable<GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse<TPayload>>
{
public GraphQLWebSocketResponse() { }
public TPayload Payload { get; set; }
public bool Equals(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse<TPayload>? other) { }
public override bool Equals(object? obj) { }
public override int GetHashCode() { }
}
public enum GraphQLWebsocketConnectionState
{
Disconnected = 0,
Connecting = 1,
Connected = 2,
}
public interface IGraphQLWebSocketClient : GraphQL.Client.Abstractions.IGraphQLClient
{
System.IObservable<object?> PongStream { get; }
System.IObservable<System.Exception> WebSocketReceiveErrors { get; }
string? WebSocketSubProtocol { get; }
System.IObservable<GraphQL.Client.Abstractions.Websocket.GraphQLWebsocketConnectionState> WebsocketConnectionState { get; }
System.Threading.Tasks.Task InitializeWebsocketConnection();
System.Threading.Tasks.Task SendPingAsync(object? payload);
System.Threading.Tasks.Task SendPongAsync(object? payload);
}
public interface IGraphQLWebsocketJsonSerializer : GraphQL.Client.Abstractions.IGraphQLJsonSerializer
{
GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse<TResponse> DeserializeToWebsocketResponse<TResponse>(byte[] bytes);
System.Threading.Tasks.Task<GraphQL.Client.Abstractions.Websocket.WebsocketMessageWrapper> DeserializeToWebsocketResponseWrapperAsync(System.IO.Stream stream);
byte[] SerializeToBytes(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request);
}
public class WebsocketMessageWrapper : GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse
{
public WebsocketMessageWrapper() { }
[System.Runtime.Serialization.IgnoreDataMember]
public byte[] MessageBytes { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace GraphQL.Client.Abstractions
{
public static class GraphQLClientExtensions
{
public static System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, GraphQL.GraphQLRequest request, System.Func<TResponse> defineResponseType) { }
public static System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, GraphQL.GraphQLRequest request, System.Func<TResponse> defineResponseType, System.Action<System.Exception> exceptionHandler) { }
public static System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, GraphQL.GraphQLRequest request, System.Func<TResponse> defineResponseType, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, string query, object? variables = null, string? operationName = null, System.Func<TResponse> defineResponseType = null, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, GraphQL.GraphQLRequest request, System.Func<TResponse> defineResponseType, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, string query, object? variables = null, string? operationName = null, System.Func<TResponse> defineResponseType = null, System.Threading.CancellationToken cancellationToken = default) { }
}
public static class GraphQLJsonSerializerExtensions
{
public static TOptions AndReturn<TOptions>(this System.Action<TOptions> configure, TOptions options) { }
public static TOptions New<TOptions>(this System.Action<TOptions> configure) { }
}
public interface IGraphQLClient
{
System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(GraphQL.GraphQLRequest request);
System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(GraphQL.GraphQLRequest request, System.Action<System.Exception> exceptionHandler);
System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(GraphQL.GraphQLRequest request, System.Threading.CancellationToken cancellationToken = default);
System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(GraphQL.GraphQLRequest request, System.Threading.CancellationToken cancellationToken = default);
}
public interface IGraphQLJsonSerializer
{
System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> DeserializeFromUtf8StreamAsync<TResponse>(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken);
string SerializeToString(GraphQL.GraphQLRequest request);
}
}
namespace GraphQL.Client.Abstractions.Utilities
{
public static class StringExtensions
{
public static string Capitalize(this string str) { }
public static string StripIndent(this string str) { }
public static string ToCamelCase(this string str) { }
public static string ToConstantCase(this string str) { }
public static string ToKebabCase(this string str) { }
public static string ToLowerCase(this string str) { }
public static string ToLowerFirst(this string str) { }
public static string ToPascalCase(this string str) { }
public static string ToSnakeCase(this string str) { }
public static string ToUpperCase(this string str) { }
public static string ToUpperFirst(this string str) { }
public static System.Collections.Generic.IEnumerable<string> ToWords(this string str) { }
}
public static class StringUtils
{
public static string Capitalize(string str) { }
public static string ChangeCase(string str, System.Func<string, string> composer) { }
public static string ChangeCase(string str, System.Func<string, int, string> composer) { }
public static string ChangeCase(string str, string sep, System.Func<string, string> composer) { }
public static string ChangeCase(string str, string sep, System.Func<string, int, string> composer) { }
public static string StripIndent(string str) { }
public static string ToCamelCase(string str) { }
public static string ToConstantCase(string str) { }
public static string ToKebabCase(string str) { }
public static string ToLowerCase(string str) { }
public static string ToLowerFirst(string str) { }
public static string ToPascalCase(string str) { }
public static string ToSnakeCase(string str) { }
public static string ToUpperCase(string str) { }
public static string ToUpperFirst(string str) { }
public static System.Collections.Generic.IEnumerable<string> ToWords(string str) { }
}
}
21 changes: 21 additions & 0 deletions tests/GraphQL.Client.ApiTests/GraphQL.Client.ApiTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="../tests.props" />

<PropertyGroup>
<TargetFramework>net7</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="PublicApiGenerator" Version="11.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\GraphQL.Client.LocalExecution\GraphQL.Client.LocalExecution.csproj" />
<ProjectReference Include="..\..\src\GraphQL.Client.Serializer.Newtonsoft\GraphQL.Client.Serializer.Newtonsoft.csproj" />
<ProjectReference Include="..\..\src\GraphQL.Client.Serializer.SystemTextJson\GraphQL.Client.Serializer.SystemTextJson.csproj" />
<ProjectReference Include="..\..\src\GraphQL.Client\GraphQL.Client.csproj" />
</ItemGroup>

</Project>
Loading