Skip to content

Commit

Permalink
Implemented Incident Batch Resolution Saga
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Feb 6, 2024
1 parent cc14f75 commit 0ac4a08
Show file tree
Hide file tree
Showing 18 changed files with 381 additions and 144 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.Collections.Immutable;
using Alba;
using FluentAssertions;
using Helpdesk.Api.Incidents;
using Helpdesk.Api.Incidents.ResolutionBatch;
using Helpdesk.Api.Tests.Incidents.Fixtures;
using Xunit;
using Wolverine.Http;
using Wolverine.Tracking;

namespace Helpdesk.Api.Tests.Incidents;

public class BatchResolutionTests(AppFixture fixture): IntegrationContext(fixture)
{
[Fact]
public async Task InitiateBatch_ShouldSucceed()
{
// Given
List<Guid> incidents =
[
(await Host.LoggedIncident()).Id,
(await Host.LoggedIncident()).Id,
(await Host.LoggedIncident()).Id
];

// When
var result = await Host.Scenario(x =>
{
x.Post.Json(new InitiateIncidentsBatchResolution(incidents, agentId, resolution))
.ToUrl($"/api/agents/{agentId}/incidents/resolve");
x.StatusCodeShouldBe(201);
});

// Then
// Check the HTTP Response
var response = await result.ReadAsJsonAsync<CreationResponse>();
response.Should().NotBeNull();
response!.Url.Should().StartWith("/api/incidents/resolution/");

// Check if details are available
result = await Host.Scenario(x =>
{
x.Get.Url(response.Url);
x.StatusCodeShouldBeOk();
});

var updated = await result.ReadAsJsonAsync<IncidentsBatchResolution>();
updated.Should().BeEquivalentTo(
new IncidentsBatchResolution(
response.GetCreatedId("/api/incidents/resolution/"),
incidents.ToImmutableDictionary(ks => ks, _ => ResolutionStatus.Pending),
ResolutionStatus.Pending,
1
)
);
}

[Fact]
public async Task Batch_ShouldComplete()
{
// Given
List<Guid> incidents =
[
(await Host.LoggedIncident()).Id,
(await Host.LoggedIncident()).Id,
(await Host.LoggedIncident()).Id
];

var (session, result) = await TrackedHttpCall(x =>
{
x.Post.Json(new InitiateIncidentsBatchResolution(incidents, agentId, resolution))
.ToUrl($"/api/agents/{agentId}/incidents/resolve");
x.StatusCodeShouldBe(201);
});
var creationResponse = await result.ReadAsJsonAsync<CreationResponse>();
var batchId = creationResponse!.GetCreatedId("/api/incidents/resolution/");

// Then
session.Status.Should().Be(TrackingStatus.Completed);

// Check if details are available
result = await Host.Scenario(x =>
{
x.Get.Url($"/api/incidents/resolution/{batchId}");
x.StatusCodeShouldBeOk();
});

var updated = await result.ReadAsJsonAsync<IncidentsBatchResolution>();
updated.Should().NotBeNull();
updated.Should().BeEquivalentTo(
new IncidentsBatchResolution(
batchId,
incidents.ToImmutableDictionary(ks => ks, _ => ResolutionStatus.Resolved),
ResolutionStatus.Resolved,
incidents.Count + 2 // for initiated and completed
)
);
}

private readonly Guid agentId = Guid.NewGuid();
private readonly ResolutionType resolution = ResolutionType.Permanent;
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,15 @@ public static async Task<Guid> GetCreatedId(this IScenarioResult result)
response.Should().NotBeNull();
response!.Url.Should().StartWith("/api/incidents/");

return response.GetCreatedId();
return response.GetCreatedId("/api/incidents/");
}

public static Guid GetCreatedId(this CreationResponse response)
public static Guid GetCreatedId(this CreationResponse response, string urlPrefix)
{
response.Url.Should().StartWith("/api/incidents/");
response.Url.Should().StartWith(urlPrefix);

var createdId = response.Url["/api/incidents/".Length..];
var uri = new Uri(response.Url.Split("?")[0]);
var createdId = uri.Segments.Last();

if (!Guid.TryParse(createdId, out var guid))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public async Task LogIncident_ShouldSucceed()

await Host.IncidentDetailsShouldBe(
new IncidentDetails(
response.GetCreatedId(),
response.GetCreatedId("/api/incidents/"),
CustomerId,
IncidentStatus.Pending,
[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public async Task<IScenarioResult> Scenario(Action<Scenario> configure)
// 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)
protected async Task<(ITrackedSession, IScenarioResult)> TrackedHttpCall(Action<Scenario> configuration, int timeout = 5000)
{
IScenarioResult? result = null;

Expand All @@ -117,7 +117,7 @@ protected async Task<(ITrackedSession, IScenarioResult)> TrackedHttpCall(Action<
// The inner part here is actually making an HTTP request
// to the system under test with Alba
result = await Host.Scenario(configuration);
});
}, timeout);

result.Should().NotBeNull();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// using System.Reflection;
// using JasperFx.CodeGeneration.Frames;
// using JasperFx.CodeGeneration.Model;
// using Microsoft.AspNetCore.Http.Metadata;
// using Microsoft.Extensions.Primitives;
// using Wolverine.Http;
//
// namespace Helpdesk.Api.Core.Http;
//
// public record AcceptedResponse(string Url) : IHttpAware, IEndpointMetadataProvider
// {
// public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
// {
// builder.RemoveStatusCodeResponse(200);
//
// var create = new MethodCall(method.DeclaringType!, method).Creates.FirstOrDefault()?.VariableType;
// var metadata = new WolverineProducesResponseTypeMetadata { Type = create, StatusCode = 201 };
// builder.Metadata.Add(metadata);
// }
//
// void IHttpAware.Apply(HttpContext context)
// {
// context.Response.Headers.Location = Url;
// context.Response.StatusCode = 201;
// }
//
// public static CreationResponse<T> For<T>(T value, string url) => new CreationResponse<T>(url, value);
// }
4 changes: 0 additions & 4 deletions Sample/Helpdesk.Wolverine/Helpdesk.Api/Helpdesk.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,4 @@
<PackageReference Include="WolverineFx.Marten" Version="2.0.0-alpha.1"/>
<PackageReference Include="WolverineFx.Http" Version="2.0.0-alpha.1"/>
</ItemGroup>

<ItemGroup>
<Folder Include="Internal\Generated\"/>
</ItemGroup>
</Project>
50 changes: 50 additions & 0 deletions Sample/Helpdesk.Wolverine/Helpdesk.Api/Incidents/Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Helpdesk.Api.Incidents.GettingCustomerIncidents;
using Helpdesk.Api.Incidents.GettingCustomerIncidentsSummary;
using Helpdesk.Api.Incidents.GettingDetails;
using Helpdesk.Api.Incidents.GettingHistory;
using Helpdesk.Api.Incidents.ResolutionBatch;
using Helpdesk.Api.Incidents.Resolving;
using Marten;
using Marten.Events.Projections;
using Wolverine;

namespace Helpdesk.Api.Incidents;

public static class Configuration
{
public static StoreOptions ConfigureIncidents(this StoreOptions options)
{
options.Projections.LiveStreamAggregation<Incident>();
options.Projections.LiveStreamAggregation<IncidentsBatchResolution>();
options.Projections.Add<IncidentHistoryTransformation>(ProjectionLifecycle.Inline);
options.Projections.Add<IncidentDetailsProjection>(ProjectionLifecycle.Inline);
options.Projections.Add<IncidentShortInfoProjection>(ProjectionLifecycle.Inline);
options.Projections.Add<CustomerIncidentsSummaryProjection>(ProjectionLifecycle.Async);

return options;
}

public static WolverineOptions ConfigureIncidents(this WolverineOptions options)
{
//Console.WriteLine(options.DescribeHandlerMatch(typeof(ResolveFromBatchHandler)));
options.LocalQueue("incidents_batch_resolution")
.Sequential();

options.Publish(rule =>
{
rule.Message<InitiateIncidentsBatchResolution>();
rule.Message<ResolveIncidentFromBatch>()
.Message<IncidentResolved>()
.Message<IncidentResolutionFailed>();
rule.MessagesImplementing<IncidentsBatchResolutionEvent>();
rule
.ToLocalQueue("incidents_batch_resolution")
.UseDurableInbox();
});

return options;
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
using Marten;
using Marten.AspNetCore;
using Microsoft.AspNetCore.Mvc;
using Wolverine.Http;

namespace Helpdesk.Api.Incidents.GettingCustomerIncidentsSummary;

public static class GetCustomerIncidentsSummaryEndpoint
{
// That for some reason doesn't work for me
// [WolverineGet("/api/customers/{customerId:guid}/incidents/incidents-summary")]
// public static Task GetCustomerIncidentsSummary([FromRoute] Guid customerId, HttpContext context,
// IQuerySession querySession) =>
// querySession.Json.WriteById<CustomerIncidentsSummary>(customerId, context);

[WolverineGet("/api/customers/{customerId:guid}/incidents/incidents-summary")]
public static Task<CustomerIncidentsSummary?> GetCustomerIncidentsSummary(
[FromRoute] Guid customerId,
IQuerySession querySession,
CancellationToken ct
) =>
querySession.LoadAsync<CustomerIncidentsSummary>(customerId, ct);

public static Task GetCustomerIncidentsSummary([FromRoute] Guid customerId, HttpContext context,
IQuerySession querySession) =>
querySession.Json.WriteById<CustomerIncidentsSummary>(customerId, context);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
using Marten;
using Marten.AspNetCore;
using Microsoft.AspNetCore.Mvc;
using Wolverine.Http;

namespace Helpdesk.Api.Incidents.GettingDetails;

public static class GetDetailsEndpoints
{
// That for some reason doesn't work for me
// [WolverineGet("/api/incidents/{incidentId:guid}")]
// public static Task GetIncidentById([FromRoute] Guid incidentId, IQuerySession querySession, HttpContext context) =>
// querySession.Json.WriteById<IncidentDetails>(incidentId, context);

[WolverineGet("/api/incidents/{incidentId:guid}")]
public static Task<IncidentDetails?> GetDetails([FromRoute] Guid incidentId, IQuerySession querySession,
CancellationToken ct) =>
querySession.LoadAsync<IncidentDetails>(incidentId, ct);
public static Task GetIncidentById([FromRoute] Guid incidentId, IQuerySession querySession, HttpContext context) =>
querySession.Json.WriteById<IncidentDetails>(incidentId, context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public IncidentHistory Transform(IEvent<AgentRespondedToIncident> input)

public IncidentHistory Transform(IEvent<IncidentResolved> input)
{
var (incidentId, resolution, resolvedBy, resolvedAt) = input.Data;
var (incidentId, resolution, resolvedBy, resolvedAt, _) = input.Data;

return new IncidentHistory(
CombGuidIdGeneration.NewGuid(),
Expand Down
3 changes: 2 additions & 1 deletion Sample/Helpdesk.Wolverine/Helpdesk.Api/Incidents/Incident.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public record IncidentResolved(
Guid IncidentId,
ResolutionType Resolution,
Guid ResolvedBy,
DateTimeOffset ResolvedAt
DateTimeOffset ResolvedAt,
Guid IncidentsBatchResolutionId
);

public record IncidentUnresolved(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Marten;
using Microsoft.AspNetCore.Mvc;
using Wolverine.Http;

namespace Helpdesk.Api.Incidents.ResolutionBatch.GettingIncidentResolutionBatch;

public class GetIncidentsBatchResolution
{
[WolverineGet("/api/incidents/resolution/{batchId:guid}")]
public static Task<IncidentsBatchResolution?> Get([FromRoute] Guid batchId, [FromQuery]long version, IQuerySession querySession,
CancellationToken ct) =>
querySession.Events.AggregateStreamAsync<IncidentsBatchResolution>(batchId, version: version, token: ct);
}
Loading

0 comments on commit 0ac4a08

Please sign in to comment.