Tutorial

7.6 Test it

The host tests speak GraphQL now. A small helper posts a query and returns the response.

tests/Shopfront.Tests/IntegrationTests/GraphQL.cs:

using System.Net.Http.Json;
using System.Text.Json;
 
namespace Shopfront.Tests.IntegrationTests;
 
internal static class GraphQL
{
    public static async Task<JsonElement> Post(
        this HttpClient client,
        string query,
        object? variables = null
    )
    {
        var response = await client.PostAsJsonAsync("/trax/graphql", new { query, variables });
        return (await response.Content.ReadFromJsonAsync<JsonElement>())!;
    }
 
    // The extensions.code of each error, empty when there are none.
    public static IEnumerable<string?> ErrorCodes(this JsonElement response) =>
        response.TryGetProperty("errors", out var errors)
            ? errors
                .EnumerateArray()
                .Select(e =>
                    e.TryGetProperty("extensions", out var x) && x.TryGetProperty("code", out var c)
                        ? c.GetString()
                        : null
                )
            : [];
}

tests/Shopfront.Tests/IntegrationTests/HostTests.cs:

using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Shopfront.Auth;
 
namespace Shopfront.Tests.IntegrationTests;
 
[TestFixture]
public class HostTests
{
    private const string PlaceOrder = """
        mutation Place($input: PlaceOrderInput!) {
          dispatch { placeOrder(input: $input) { externalId output { orderId total } } }
        }
        """;
 
    private const string Orders = "{ discover { orders { nodes { customerId total } } } }";
 
    private WebApplicationFactory<Program> _app = null!;
 
    [SetUp]
    public async Task StartTheApplication()
    {
        await TestDatabase.Reset();
        _app = new WebApplicationFactory<Program>().WithWebHostBuilder(host =>
            host.UseEnvironment("Development")
                .UseSetting("ConnectionStrings:Shopfront", TestDatabase.ConnectionString)
        );
    }
 
    [TearDown]
    public ValueTask StopTheApplication() => _app.DisposeAsync();
 
    private HttpClient SignedIn(string user, string role)
    {
        var client = _app.CreateClient();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Bearer",
            DevJwt.Mint(user, role)
        );
        return client;
    }
 
    private static object Order(string sku, string paymentToken = "tok_visa") =>
        new
        {
            input = new { paymentToken, lines = new[] { new { sku, quantity = 1 } } },
        };
 
    private static JsonElement Placed(JsonElement response) =>
        response.GetProperty("data").GetProperty("dispatch").GetProperty("placeOrder");
 
    [Test]
    public async Task Products_Anonymous_ListsTheCatalogue()
    {
        var response = await _app.CreateClient()
            .Post("{ discover { products(order: { sku: ASC }) { nodes { sku } } } }");
 
        response
            .GetProperty("data")
            .GetProperty("discover")
            .GetProperty("products")
            .GetProperty("nodes")
            .EnumerateArray()
            .Select(p => p.GetProperty("sku").GetString())
            .Should()
            .Equal("CAP", "MUG", "TEE");
    }
 
    [Test]
    public async Task PlaceOrder_Anonymous_IsNotAuthorized()
    {
        var response = await _app.CreateClient().Post(PlaceOrder, Order("MUG"));
 
        response.ErrorCodes().Should().Contain("TRAX_AUTHORIZATION");
    }
 
    [Test]
    public async Task PlaceOrder_Staff_IsNotAuthorized()
    {
        var response = await SignedIn("lin", Roles.Staff).Post(PlaceOrder, Order("MUG"));
 
        response.ErrorCodes().Should().Contain("TRAX_AUTHORIZATION");
    }
 
    [Test]
    public async Task PlaceOrder_Customer_AnswersTheOrder()
    {
        var response = await SignedIn("ada", Roles.Customer).Post(PlaceOrder, Order("MUG"));
 
        Placed(response).GetProperty("output").GetProperty("total").GetDecimal().Should().Be(12.00m);
    }
 
    [Test]
    public async Task PlaceOrder_Declined_AnswersTheReason()
    {
        var response = await SignedIn("ada", Roles.Customer)
            .Post(PlaceOrder, Order("MUG", "tok_chargeDeclined"));
 
        response.ErrorCodes().Should().Contain("TRAX_TRAIN_ERROR");
        response
            .GetProperty("errors")[0]
            .GetProperty("message")
            .GetString()
            .Should()
            .Be("The card was declined.");
    }
 
    [Test]
    public async Task Orders_EachCustomerSeesTheirOwn_StaffSeeAll()
    {
        await SignedIn("ada", Roles.Customer).Post(PlaceOrder, Order("MUG"));
        await SignedIn("grace", Roles.Customer).Post(PlaceOrder, Order("MUG"));
 
        static IEnumerable<string?> Owners(JsonElement response) =>
            response
                .GetProperty("data")
                .GetProperty("discover")
                .GetProperty("orders")
                .GetProperty("nodes")
                .EnumerateArray()
                .Select(o => o.GetProperty("customerId").GetString());
 
        Owners(await SignedIn("grace", Roles.Customer).Post(Orders))
            .Should()
            .Equal("TraxJwt:grace");
        Owners(await SignedIn("lin", Roles.Staff).Post(Orders))
            .Should()
            .BeEquivalentTo(["TraxJwt:ada", "TraxJwt:grace"]);
    }
 
    [Test]
    public async Task RunProgress_OnlyTheStarter_CanFollowTheRun()
    {
        const string progress = """
            query Progress($runId: String!) {
              runProgress(runId: $runId) { state steps { name } problem { code } }
            }
            """;
        var placed = Placed(await SignedIn("ada", Roles.Customer).Post(PlaceOrder, Order("MUG")));
        var runId = new { runId = placed.GetProperty("externalId").GetString() };
 
        var mine = (await SignedIn("ada", Roles.Customer).Post(progress, runId))
            .GetProperty("data")
            .GetProperty("runProgress");
        var hers = (await SignedIn("grace", Roles.Customer).Post(progress, runId))
            .GetProperty("data")
            .GetProperty("runProgress");
 
        mine.GetProperty("state").GetString().Should().Be("COMPLETED");
        mine.GetProperty("steps")
            .EnumerateArray()
            .Select(s => s.GetProperty("name").GetString())
            .Should()
            .StartWith("IdentifyCustomerJunction");
        hers.GetProperty("problem").GetProperty("code").GetString().Should().Be("NOT_FOUND");
    }
}
dotnet test
Passed!  - Failed:     0, Passed:    51, Skipped:     0, Total:    51, Duration: 27 s - Shopfront.Tests.dll (net10.0)

What we have

Shopfront serves a GraphQL schema that Trax built from what was already there: the catalogue for anyone, PlaceOrder for customers, each customer's own orders, and the progress of a run for the one customer who started it. Not a line of it repeats a rule: the roles are on the train, the rows on the context, and the schema follows both.

Staff can read orders, but not yet what happened to them: which runs failed, where and why. The next chapter gives them the operations console.

SDK Reference

AddTraxGraphQL | TraxMutation | Query models | Run progress | AddJunctionEvents | dispatch mutations


Next: 8. The operations console