Skip to content

Commit

Permalink
WIP: New "official" incident service example project
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydmiller committed Jan 21, 2025
1 parent 41b104b commit ba66c36
Show file tree
Hide file tree
Showing 18 changed files with 854 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<IsPackable>false</IsPackable>
<TargetFrameworks>net9.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Alba" Version="8.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.9.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\IncidentService\IncidentService.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Alba;
using Marten;
using Microsoft.Extensions.DependencyInjection;
using Oakton;
using Wolverine.Runtime;
using Wolverine.Tracking;
using Xunit;

namespace IncidentService.Tests;

public class AppFixture : IAsyncLifetime
{
public IAlbaHost? Host { get; private set; }

public async Task InitializeAsync()
{
OaktonEnvironment.AutoStartHost = true;

// This is bootstrapping the actual application using
// its implied Program.Main() set up
Host = await AlbaHost.For<Program>(x => { });
}

public async Task DisposeAsync()
{
await Host!.StopAsync();
Host.Dispose();
}
}

[CollectionDefinition("integration")]
public class IntegrationCollection : ICollectionFixture<AppFixture>;

[Collection("integration")]
public abstract class IntegrationContext : IAsyncLifetime
{
private readonly AppFixture _fixture;

protected IntegrationContext(AppFixture fixture)
{
_fixture = fixture;
Runtime = (WolverineRuntime)fixture.Host!.Services.GetRequiredService<IWolverineRuntime>();
}

public WolverineRuntime Runtime { get; }

public IAlbaHost Host => _fixture.Host!;
public IDocumentStore Store => _fixture.Host!.Services.GetRequiredService<IDocumentStore>();


async Task IAsyncLifetime.InitializeAsync()
{
// Using Marten, wipe out all data and reset the state
// back to exactly what we described in InitialAccountData
await Store.Advanced.ResetAllData();
}

// This is required because of the IAsyncLifetime
// interface. Note that I do *not* tear down database
// state after the test. That's purposeful
public Task DisposeAsync()
{
return Task.CompletedTask;
}

public Task<IScenarioResult> Scenario(Action<Scenario> configure)
{
return Host.Scenario(configure);
}

// This method allows us to make HTTP calls into our system
// in memory with Alba, but do so within Wolverine's test support
// for message tracking to both record outgoing messages and to ensure
// that any cascaded work spawned by the initial command is completed
// before passing control back to the calling test
protected async Task<(ITrackedSession, IScenarioResult)> TrackedHttpCall(Action<Scenario> configuration)
{
IScenarioResult result = null!;

// The outer part is tying into Wolverine's test support
// to "wait" for all detected message activity to complete
var tracked = await Host.ExecuteAndWaitAsync(async () =>
{
// The inner part here is actually making an HTTP request
// to the system under test with Alba
result = await Host.Scenario(configuration);
});

return (tracked, result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Helpdesk.Api.Incidents;
using Shouldly;
using Wolverine.Http;
using Xunit;

namespace IncidentService.Tests;

public class when_logging_an_incident : IntegrationContext
{
public when_logging_an_incident(AppFixture fixture) : base(fixture)
{
}

[Fact]
public void unit_test()
{
var contact = new Contact(ContactChannel.Email);
var command = new LogIncident(Guid.NewGuid(), contact, "It's broken", Guid.NewGuid());

var (response, startStream) = LogIncidentEndpoint.Post(command);

// Should only have the one event
startStream.Events.ShouldBe([
new IncidentLogged(command.CustomerId, command.Contact, command.Description, command.LoggedBy)
]);
}

[Fact]
public async Task happy_path_end_to_end()
{
var contact = new Contact(ContactChannel.Email);
var command = new LogIncident(Guid.NewGuid(), contact, "It's broken", Guid.NewGuid());

// Log a new incident first
var initial = await Scenario(x =>
{
x.Post.Json(command).ToUrl("/api/incidents");
x.StatusCodeShouldBe(201);
});

// Read the response body by deserialization
var response = initial.ReadAsJson<CreationResponse<Guid>>();

// Reaching into Marten to build the current state of the new Incident
// just to check the expected outcome
using var session = Store.LightweightSession();
var incident = await session.Events.AggregateStreamAsync<Incident>(response.Value);

incident.Status.ShouldBe(IncidentStatus.Pending);
}
}
14 changes: 14 additions & 0 deletions src/Samples/IncidentService/IncidentService/ArchiveIncident.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Marten;

namespace IncidentService;

public record ArchiveIncident(Guid IncidentId);

public static class ArchiveIncidentHandler
{
// Just going to code this one pretty crudely
public static void Handle(ArchiveIncident command, IDocumentSession session)
{
// TODO -- do more here w/ Jeffry
}
}
33 changes: 33 additions & 0 deletions src/Samples/IncidentService/IncidentService/CategoriseIncident.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Helpdesk.Api.Incidents;
using Microsoft.AspNetCore.Mvc;
using Wolverine.Http;
using Wolverine.Http.Marten;

namespace IncidentService;

public record CategoriseIncident(
Guid IncidentId,
IncidentCategory Category,
Guid CategorisedBy,
int Version
);

public static class CategoriseIncidentEndpoint
{
public static ProblemDetails Validate(Incident incident)
{
return incident.Status == IncidentStatus.Closed
? new ProblemDetails { Detail = "Incident is already closed" }

// All good, keep going!
: WolverineContinue.NoProblems;
}

[WolverinePost("/api/incidents/{incidentId:guid}/category")]
public static IncidentCategorised Post(
CategoriseIncident command,
[Aggregate("incidentId")] Incident incident, DateTimeOffset now)
{
return new IncidentCategorised(command.IncidentId, command.Category, command.CategorisedBy);
}
}
51 changes: 51 additions & 0 deletions src/Samples/IncidentService/IncidentService/CloseIncident.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using JasperFx.Core;
using Wolverine;
using Wolverine.Http;
using Wolverine.Http.Marten;
using Wolverine.Marten;

namespace IncidentService;

public record CloseIncident(
Guid ClosedBy,
int Version
);

public static class CloseIncidentHandler
{
[WolverinePost("/api/incidents/close/{id}")]
public static (UpdatedAggregate, Events, OutgoingMessages) Handle(
CloseIncident command,

[Aggregate]
Incident incident)
{
/* More logic for later
if (current.Status is not IncidentStatus.ResolutionAcknowledgedByCustomer)
throw new InvalidOperationException("Only incident with acknowledged resolution can be closed");
if (current.HasOutstandingResponseToCustomer)
throw new InvalidOperationException("Cannot close incident that has outstanding responses to customer");
*/


if (incident.Status == IncidentStatus.Closed)
{
return (new UpdatedAggregate(), [], []);
}

return (

// Returning the latest view of
// the Incident as the actual response body
new UpdatedAggregate(),

// New event to be appended to the Incident stream
[new IncidentClosed(command.ClosedBy)],

// Getting fancy here, telling Wolverine to schedule a
// command message for three days from now
[new ArchiveIncident(incident.Id).DelayedFor(3.Days())]);
}
}
12 changes: 12 additions & 0 deletions src/Samples/IncidentService/IncidentService/GetIncidentEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Wolverine.Http;

namespace IncidentService;

public static class GetIncidentEndpoint
{
[WolverineGet("/api/incidents/{id}")]
public static async Task Get(Guid id)
{
throw new NotImplementedException();
}
}
Loading

0 comments on commit ba66c36

Please sign in to comment.