| | | 1 | | using System.Net; |
| | | 2 | | using System.Reflection; |
| | | 3 | | using ArturRios.Configuration.Enums; |
| | | 4 | | using ArturRios.Output; |
| | | 5 | | using ArturRios.Util.Http; |
| | | 6 | | using ArturRios.Util.Test.Exceptions; |
| | | 7 | | using ArturRios.Util.WebApi.Security.Records; |
| | | 8 | | using Microsoft.AspNetCore.Mvc.Testing; |
| | | 9 | | using Newtonsoft.Json; |
| | | 10 | | using Newtonsoft.Json.Serialization; |
| | | 11 | | |
| | | 12 | | namespace ArturRios.Util.Test.Functional; |
| | | 13 | | |
| | | 14 | | /// <summary> |
| | | 15 | | /// Base class for functional web API tests. It spins up an in-memory host with |
| | | 16 | | /// <see cref="WebApplicationFactory{TEntryPoint}"/> and exposes an <see cref="HttpGateway"/> plus helpers |
| | | 17 | | /// for authenticating and authorizing requests. Derive from it and pass the target environment. |
| | | 18 | | /// </summary> |
| | | 19 | | /// <typeparam name="T">The entry point type of the web API under test (typically its <c>Program</c> class).</typeparam> |
| | | 20 | | public class WebApiTest<T> : IDisposable where T : class |
| | | 21 | | { |
| | | 22 | | // HttpGateway deserializes with Newtonsoft.Json, which does not populate the protected setter of |
| | | 23 | | // DataOutput<T>.Data. The auth payload is therefore parsed here with a resolver that allows it. |
| | 1 | 24 | | private static readonly JsonSerializerSettings AuthSerializerSettings = new() |
| | 1 | 25 | | { |
| | 1 | 26 | | ContractResolver = new NonPublicSetterContractResolver() |
| | 1 | 27 | | }; |
| | | 28 | | |
| | 5 | 29 | | private readonly WebApplicationFactory<T> _factory = new(); |
| | | 30 | | |
| | | 31 | | /// <summary>The gateway used to issue HTTP requests against the in-memory host.</summary> |
| | | 32 | | protected readonly HttpGateway Gateway; |
| | | 33 | | |
| | | 34 | | /// <summary>Starts the in-memory host for the given <paramref name="environment"/> and creates the gateway.</summar |
| | | 35 | | /// <param name="environment">The environment the host should run as; also sets <c>ASPNETCORE_ENVIRONMENT</c>.</para |
| | 5 | 36 | | protected WebApiTest(EnvironmentType environment) |
| | 5 | 37 | | { |
| | 5 | 38 | | Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", environment.ToString().ToLower()); |
| | | 39 | | |
| | 5 | 40 | | Gateway = new HttpGateway(_factory.CreateClient()); |
| | 5 | 41 | | } |
| | | 42 | | |
| | | 43 | | /// <summary>Authenticates against <paramref name="authRoute"/> and returns the resulting authentication payload.</s |
| | | 44 | | /// <param name="credentials">The credentials to authenticate with.</param> |
| | | 45 | | /// <param name="authRoute">The relative authentication route.</param> |
| | | 46 | | /// <returns>The authentication payload returned by the API.</returns> |
| | | 47 | | /// <exception cref="TestException">Thrown when authentication fails or returns no usable token.</exception> |
| | | 48 | | public async Task<Authentication> AuthenticateAsync(Credentials credentials, string authRoute) |
| | 3 | 49 | | { |
| | 3 | 50 | | var response = await Gateway.Client.PostAsync(authRoute, credentials.ToJsonStringContent()); |
| | 3 | 51 | | var json = await response.Content.ReadAsStringAsync(); |
| | | 52 | | |
| | 3 | 53 | | var body = response.StatusCode == HttpStatusCode.OK && !string.IsNullOrEmpty(json) |
| | 3 | 54 | | ? JsonConvert.DeserializeObject<DataOutput<Authentication>>(json, AuthSerializerSettings) |
| | 3 | 55 | | : null; |
| | | 56 | | |
| | 3 | 57 | | var authError = body is null |
| | 3 | 58 | | || !body.Success |
| | 3 | 59 | | || body.Data is null |
| | 3 | 60 | | || string.IsNullOrEmpty(body.Data.Token); |
| | | 61 | | |
| | 3 | 62 | | return authError ? throw new TestException("Could not authenticate") : body!.Data!; |
| | 2 | 63 | | } |
| | | 64 | | |
| | | 65 | | /// <summary>Adds a bearer token to the gateway's default request headers.</summary> |
| | | 66 | | /// <param name="authToken">The JWT to send as a Bearer token.</param> |
| | | 67 | | public void Authorize(string authToken) => |
| | 2 | 68 | | Gateway.Client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authToken}"); |
| | | 69 | | |
| | | 70 | | /// <summary>Authenticates and applies the resulting token to the gateway's default headers.</summary> |
| | | 71 | | /// <param name="credentials">The credentials to authenticate with.</param> |
| | | 72 | | /// <param name="authRoute">The relative authentication route.</param> |
| | | 73 | | public async Task AuthenticateAndAuthorizeAsync(Credentials credentials, string authRoute) |
| | 1 | 74 | | { |
| | 1 | 75 | | var authentication = await AuthenticateAsync(credentials, authRoute); |
| | | 76 | | |
| | 1 | 77 | | Authorize(authentication.Token!); |
| | 1 | 78 | | } |
| | | 79 | | |
| | | 80 | | /// <summary>Releases the in-memory host and its HTTP clients.</summary> |
| | | 81 | | public void Dispose() |
| | 5 | 82 | | { |
| | 5 | 83 | | Dispose(true); |
| | 5 | 84 | | GC.SuppressFinalize(this); |
| | 5 | 85 | | } |
| | | 86 | | |
| | | 87 | | /// <summary>Releases managed resources. Override to dispose additional resources in derived classes.</summary> |
| | | 88 | | /// <param name="disposing"><c>true</c> when called from <see cref="Dispose()"/>.</param> |
| | | 89 | | protected virtual void Dispose(bool disposing) |
| | 5 | 90 | | { |
| | 5 | 91 | | if (disposing) |
| | 5 | 92 | | { |
| | 5 | 93 | | _factory.Dispose(); |
| | 5 | 94 | | } |
| | 5 | 95 | | } |
| | | 96 | | |
| | | 97 | | private sealed class NonPublicSetterContractResolver : DefaultContractResolver |
| | | 98 | | { |
| | | 99 | | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) |
| | 9 | 100 | | { |
| | 9 | 101 | | var property = base.CreateProperty(member, memberSerialization); |
| | | 102 | | |
| | 9 | 103 | | if (!property.Writable && member is PropertyInfo { CanWrite: true }) |
| | 1 | 104 | | { |
| | 1 | 105 | | property.Writable = true; |
| | 1 | 106 | | } |
| | | 107 | | |
| | 9 | 108 | | return property; |
| | 9 | 109 | | } |
| | | 110 | | } |
| | | 111 | | } |