• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Shane32 / TestHelpers / 16107140908

07 Jul 2025 03:50AM UTC coverage: 80.8% (-0.2%) from 80.978%
16107140908

push

github

web-flow
Fix ServiceCollector (#3)

49 of 80 branches covered (61.25%)

Branch coverage included in aggregate %.

7 of 9 new or added lines in 1 file covered. (77.78%)

254 of 295 relevant lines covered (86.1%)

6.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.68
/src/Project/GraphQLTestBase.cs
1
using System.Collections.Concurrent;
2
using System.Net;
3
using System.Net.Http.Headers;
4
using System.Reflection;
5
using System.Security.Claims;
6
using System.Text.Json;
7
using Microsoft.AspNetCore.Hosting;
8
using Microsoft.AspNetCore.TestHost;
9
using Microsoft.Extensions.Configuration;
10
using Microsoft.Extensions.DependencyInjection;
11
using Microsoft.IdentityModel.JsonWebTokens;
12
using Microsoft.IdentityModel.Tokens;
13

14
namespace Shane32.TestHelpers;
15

16
/// <inheritdoc/>
17
public abstract class GraphQLTestBase<TStartup> : GraphQLTestBase
18
    where TStartup : class
19
{
20
    /// <inheritdoc/>
21
    protected override void ConfigureWebHostBuilder(IWebHostBuilder webHostBuilder)
22
    {
3✔
23
        base.ConfigureWebHostBuilder(webHostBuilder);
3✔
24
        webHostBuilder.UseStartup<TStartup>();
3✔
25
    }
3✔
26
}
27

28
/// <summary>
29
/// Represents the base class for GraphQL testing against a test server.
30
/// </summary>
31
public abstract class GraphQLTestBase : IDisposable, Xunit.IAsyncLifetime, IAsyncDisposable
32
{
33
    /// <summary>
34
    /// The name of the test environment.
35
    /// </summary>
36
    public const string TestEnvironmentName = "Test";
37

38
    private readonly ServiceCollection _serviceCollection = new();
3✔
39
    /// <summary>
40
    /// Gets the service collection that will be used to configure the test server.
41
    /// </summary>
42
    protected IServiceCollection ServiceCollection
43
        => _webHost == null
×
44
            ? _serviceCollection
×
45
            : throw new InvalidOperationException("Cannot configure services after service provider has been initialized; please move all mocks prior to any call to Service, Services, or Db.");
×
46

47
    private IServiceProvider? _services;
48
    /// <summary>
49
    /// Gets the service provider that will be used to resolve services during the test.
50
    /// </summary>
51
    protected IServiceProvider Services => _services ??= WebHost.Services;
×
52

53
    private IWebHost? _webHost;
54
    /// <summary>
55
    /// Gets or sets the test server that will be used to execute GraphQL queries.
56
    /// When setting this property, provide a test server that has already been started.
57
    /// </summary>
58
    protected IWebHost WebHost
59
    {
60
        get {
3✔
61
            if (_webHost == null) {
6✔
62
                var webHostBuilder = new WebHostBuilder();
3✔
63
                ConfigureWebHostBuilder(webHostBuilder);
3✔
64
                _webHost = webHostBuilder.Build();
3✔
65
                _webHost.Start();
3✔
66
            }
3✔
67
            return _webHost;
3✔
68
        }
3✔
69
        set {
×
70
            if (_webHost != null)
×
71
                throw new InvalidOperationException("Cannot set WebHost after it has been initialized");
×
72
            _webHost = value;
×
73
        }
×
74
    }
75

76
    private HttpClient? _client;
77
    /// <summary>
78
    /// Gets the HTTP client that will be used to execute GraphQL queries against the test server.
79
    /// </summary>
80
    protected HttpClient Client => _client ??= WebHost.GetTestClient();
3✔
81

82
    /// <summary>
83
    /// Gets or sets the claims that will be used to create the access token for the GraphQL queries.
84
    /// Defaults to a claim for the "aud" (audience) with the value "TestAudience", and a claim for the "iss" (issuer) with the value "TestIssuer".
85
    /// Clearing the claims list will result in no access token being sent with the GraphQL queries.
86
    /// </summary>
87
    public ClaimsList Claims { get; } = new();
15✔
88

89
    /// <summary>
90
    /// Initializes a new instance of the <see cref="GraphQLTestBase"/> class.
91
    /// </summary>
92
    public GraphQLTestBase()
3✔
93
    {
3✔
94
        Claims.Set(JwtRegisteredClaimNames.Aud, "TestAudience");
3✔
95
        Claims.Set(JwtRegisteredClaimNames.Iss, "TestIssuer");
3✔
96
    }
3✔
97

98
    private static readonly ConcurrentDictionary<Assembly, IConfiguration?> _serverConfigFiles = new();
1✔
99
    /// <summary>
100
    /// Gets the server configuration JSON from the specified assembly.
101
    /// </summary>
102
    protected virtual IConfiguration? GetServerConfig()
103
    {
3✔
104
        var assembly = GetServerConfigAssembly();
3✔
105
        return _serverConfigFiles.GetOrAdd(assembly, assembly2 => {
4✔
106
            //get all resource names from assembly, and find the one that ends with ".ServerConfig.json" or equals "ServerConfig.json" (case insensitive)
3✔
107
            var resourceName = GetServerConfigResourceName(assembly2);
1✔
108
            if (resourceName == null)
1✔
109
                return null;
1✔
110
            using var stream = assembly2.GetManifestResourceStream(resourceName)
×
111
                ?? throw new InvalidOperationException($"Resource {resourceName} was not found in assembly {assembly2.FullName}");
×
112
            var configuration = new ConfigurationBuilder()
×
113
                .AddJsonStream(stream)
×
114
                .Build();
×
115
            return configuration;
×
116
        });
4✔
117
    }
3✔
118

119
    /// <summary>
120
    /// Gets the assembly that contains the server configuration JSON.
121
    /// </summary>
122
    /// <returns></returns>
123
    protected virtual Assembly GetServerConfigAssembly() => GetType().Assembly;
3✔
124

125
    /// <summary>
126
    /// Gets the name of the server configuration JSON resource.
127
    /// </summary>
128
    protected virtual string? GetServerConfigResourceName(Assembly assembly)
129
        => assembly.GetManifestResourceNames().FirstOrDefault(n => n.EndsWith(".ServerConfig.json", StringComparison.OrdinalIgnoreCase) || n.Equals("ServerConfig.json", StringComparison.OrdinalIgnoreCase));
1!
130

131
    /// <summary>
132
    /// Builds the <see cref="WebHostBuilder"/> that will be used to create the test server.
133
    /// Call <see cref="IWebHostBuilder"/>.<see cref="Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.ConfigureTestServices(IWebHostBuilder, Action{IServiceCollection})">ConfigureTestServices</see>
134
    /// to override services from your Startup with test services.
135
    /// </summary>
136
    protected virtual void ConfigureWebHostBuilder(IWebHostBuilder webHostBuilder)
137
    {
3✔
138
        webHostBuilder
3✔
139
            .UseTestServer()
3✔
140
            .ConfigureAppConfiguration((context, config) => {
3✔
141
                // configure the environment name for identification later
3✔
142
                context.HostingEnvironment.EnvironmentName = TestEnvironmentName;
3✔
143
                // clear any default sources (e.g. environment variables)
3✔
144
                config.Sources.Clear();
3✔
145
                // add the test configuration file (named serverconfig.json) from the test class's assembly
3✔
146
                var serverConfig = GetServerConfig();
3✔
147
                if (serverConfig != null) {
3✔
148
                    config.Add(new ChainedConfigurationSource() {
×
149
                        Configuration = serverConfig,
×
150
                        ShouldDisposeConfiguration = false,
×
151
                    });
×
152
                }
×
153
            })
3✔
154
            // ensure that IConfiguration is available in the service provider
3✔
155
            .ConfigureServices((context, services) => services.AddSingleton(context.Configuration))
3✔
156
            // configure test services
3✔
157
            .ConfigureTestServices(services => {
3✔
158
                // configure the JWT bearer tokens for the test server
3✔
159
                services.ConfigureUnsignedJwtBearerTokens();
3✔
160
                // configure any services manually added by the test
3✔
161
                foreach (var service in _serviceCollection) {
9✔
NEW
162
                    services.Add(service);
×
NEW
163
                }
×
164
            });
6✔
165
    }
3✔
166

167
    /// <summary>
168
    /// Gets the path to the GraphQL endpoint.
169
    /// </summary>
170
    protected virtual string GetGraphQLPath() => "/api/graphql";
3✔
171

172
    /// <summary>
173
    /// Runs the specified GraphQL query and returns the response.
174
    /// </summary>
175
    /// <param name="query">The GraphQL query to execute.</param>
176
    /// <param name="variables">Variables in the form of either a JSON string or an object.</param>
177
    /// <exception cref="InvalidOperationException"></exception>
178
    public virtual async Task<ExecutionResponse> RunQueryAsync(string query, object? variables = null)
179
    {
3✔
180
        // note: sending as POST encoded as JSON so CSRF protection is not triggered
181
        var httpClient = Client;
3✔
182
        using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, GetGraphQLPath());
3✔
183
        var request = new {
3!
184
            query,
3✔
185
            variables = variables is string variablesString ? JsonSerializer.Deserialize(variablesString, typeof(JsonElement)) : variables
3✔
186
        };
3✔
187
        httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
3✔
188
        if (Claims.Count > 0)
3✔
189
            httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync().ConfigureAwait(false));
2✔
190
        using var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false);
3✔
191
        if (httpResponseMessage.StatusCode != HttpStatusCode.OK && httpResponseMessage.StatusCode != HttpStatusCode.BadRequest)
3✔
192
            throw new InvalidOperationException($"GraphQL request failed with status code {httpResponseMessage.StatusCode}");
1✔
193
        var responseStream = await httpResponseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);
2✔
194
        var response = JsonSerializer.Deserialize<ExecutionResponse>(responseStream)
2!
195
            ?? throw new InvalidOperationException("Null was received from GraphQL server");
2✔
196
        response.StatusCode = httpResponseMessage.StatusCode;
2✔
197
        return response;
2✔
198
    }
2✔
199

200
    /// <summary>
201
    /// Gets the list of claims that will be used to create the access token for the GraphQL queries.
202
    /// </summary>
203
    protected virtual Task<IEnumerable<Claim>> GetClaimsAsync() => Task.FromResult<IEnumerable<Claim>>(Claims);
2✔
204

205
    /// <summary>
206
    /// Creates a JWT access token based on the <see cref="Claims"/>.
207
    /// </summary>
208
    protected virtual async Task<string> GetAccessTokenAsync()
209
    {
2✔
210
        var now = DateTime.UtcNow;
2✔
211
        var descriptor = new SecurityTokenDescriptor();
2✔
212
        descriptor.Expires = now.AddMinutes(5);
2✔
213
        descriptor.NotBefore = now;
2✔
214
        descriptor.IssuedAt = now;
2✔
215
        descriptor.Subject = new ClaimsIdentity(await GetClaimsAsync().ConfigureAwait(false));
2✔
216
        var handler = new JsonWebTokenHandler();
2✔
217
        var token = handler.CreateToken(descriptor);
2✔
218
        return token;
2✔
219
    }
2✔
220

221
    /// <inheritdoc/>
222
    public virtual void Dispose()
223
    {
3✔
224
        _client?.Dispose();
3!
225
        _webHost?.Dispose();
3!
226
        GC.SuppressFinalize(this);
3✔
227
    }
3✔
228

229
    /// <inheritdoc cref="Xunit.IAsyncLifetime.InitializeAsync"/>
230
    protected virtual Task InitializeAsync() => Task.CompletedTask;
3✔
231

232
    Task Xunit.IAsyncLifetime.InitializeAsync() => InitializeAsync();
3✔
233
    Task Xunit.IAsyncLifetime.DisposeAsync() => DisposeAsync().AsTask();
3✔
234

235
    /// <inheritdoc cref="IAsyncDisposable.DisposeAsync"/>
236
    public virtual async ValueTask DisposeAsync()
237
    {
3✔
238
        _client?.Dispose();
3!
239
        if (_webHost is IAsyncDisposable asyncDisposable) {
6!
240
            await asyncDisposable.DisposeAsync().ConfigureAwait(false);
3✔
241
        } else {
3✔
242
            _webHost?.Dispose();
×
243
        }
×
244
        GC.SuppressFinalize(this);
3✔
245
    }
3✔
246
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc