Skip to content

Commit

Permalink
Add a bunch of tests
Browse files Browse the repository at this point in the history
  • Loading branch information
corstian committed May 30, 2024
1 parent c7dfcbc commit ec9f983
Show file tree
Hide file tree
Showing 19 changed files with 229 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public record Aircraft : IAggregate
public int Starts { get; init; }
public TimeSpan FlightTime { get; init; }

public Dictionary<string, (DateTime? Departure, DateTime? Arrival)>
public Dictionary<string, (DateTimeOffset? Departure, DateTimeOffset? Arrival)>
Flights
{ get; init; } = new();
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
using FluentResults;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Events;
using Whaally.Domain.Abstractions.Command;

namespace Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Commands;

[Immutable]
[GenerateSerializer]
public record CorrectTotalFlightCount(
int FlightCount,
string Reason) : ICommand;
int FlightCount) : ICommand;

public class CorrectTotalFlightCountHandler : ICommandHandler<Aircraft, CorrectTotalFlightCount>
{
public IResultBase Evaluate(ICommandHandlerContext<Aircraft> context, CorrectTotalFlightCount command)
=> Result.Ok();
}
{
context.StageEvent(new TotalFlightCountCorrected(DateTimeOffset.UtcNow, command.FlightCount));

return Result.Ok();
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
using FluentResults;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Events;
using Whaally.Domain.Abstractions.Command;

namespace Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Commands;

[Immutable]
[GenerateSerializer]
public record CorrectTotalFlightTime(
TimeSpan TotalTime,
string Reason) : ICommand;
TimeSpan TotalTime) : ICommand;

public class CorrectTotalFlightTimeHandler : ICommandHandler<Aircraft, CorrectTotalFlightTime>
{
public IResultBase Evaluate(ICommandHandlerContext<Aircraft> context, CorrectTotalFlightTime command)
=> Result.Ok();
}
{
context.StageEvent(new TotalFlightTimeCorrected(DateTimeOffset.UtcNow, command.TotalTime));

return Result.Ok();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ public class RemoveFlightHandler : ICommandHandler<Aircraft, RemoveFlight>
{
public IResultBase Evaluate(ICommandHandlerContext<Aircraft> context, RemoveFlight command)
{
context.StageEvent(new FlightRemoved(
command.FlightId));
var result = Result.Merge(
Result.FailIf(context.Aggregate.Flights.All(q => q.Key != command.FlightId), "Aircraft has not flown this flight"));

if (result.IsSuccess)
context.StageEvent(new FlightRemoved(command.FlightId));

return Result.Ok();
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Events;
[Immutable]
[GenerateSerializer]
public record TotalFlightCountCorrected(
DateTime Timestamp,
DateTimeOffset Timestamp,
int Number) : IEvent;

public class TotalFlightCountCorrectedHandler : IEventHandler<Aircraft, TotalFlightCountCorrected>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Events;
[Immutable]
[GenerateSerializer]
public record TotalFlightTimeCorrected(
DateTime Timestamp,
DateTimeOffset Timestamp,
TimeSpan FlightTime) : IEvent;

public class TotalFlightTimeCorrectedHandler : IEventHandler<Aircraft, TotalFlightTimeCorrected>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,28 @@
using Skyhop.Domain.FlightContext.Aggregates.FlightAggregate.Snapshots;
using Whaally.Domain.Abstractions.Saga;

namespace Skyhop.Domain.AircraftContext.Sagas;
namespace Skyhop.Domain.FlightContext.Sagas;

internal class OnAircraftChanged : ISaga<AircraftSet>
/*
* Placement of this saga is tricky.
*
* While it responds to a change on the FlightAggregate, the change it triggers is on the AircraftAggregate.
* Which of the contexts is then responsible for holding this saga?
*
* Do we make upstream responsible for notifying downstream, or can downstream subscribe to whatever events they need?
* Decisions like these impact the way people interact with this codebase, and therefore should be thought about.
*
* Naming is also important here, as depending on the context it can imply different things.
*
* If we'd move this saga to the AircraftContext, it'd better be named something like "OnFlightsDeclaredAircraftChange"
* or something.
*
* Interestingly the saga doesn't have anything to do with the FlightContext whatsoever, so for this reason it would fit
* better with the AircraftContext.
*/


public class OnAircraftChanged : ISaga<AircraftSet>
{
public async Task<IResultBase> Evaluate(ISagaContext context, AircraftSet @event)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace Whaally.Domain.Aggregate;

internal class DefaultAggregateHandlerFactory : IAggregateHandlerFactory
public class DefaultAggregateHandlerFactory : IAggregateHandlerFactory
{
private readonly Dictionary<string, IAggregateHandler> _dictionary = new();
protected readonly Dictionary<string, IAggregateHandler> _dictionary = new();
private readonly IServiceProvider _serviceProvider;

public DefaultAggregateHandlerFactory(IServiceProvider serviceProvider)
Expand Down
4 changes: 4 additions & 0 deletions src/Whaally.Domain/Saga/SagaContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public SagaContext(IServiceProvider services)

// ToDo: I do not know about situations in which no aggregateId would be provided,
// though I do not have a decent way to supply the aggregate id.

/// <summary>
/// The id of the aggregate on which the event resulting in this sagas evaluation had been applied.
/// </summary>
public string? AggregateId { get; init; }

public void StageCommand(string aggregateId, ICommand command)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using FluentAssertions;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Commands;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Events;
using Whaally.Domain.Event;

namespace Skyhop.Domain.Tests.AircraftContext.Aggregates.Commands;

public abstract class CorrectTotalFlightCountTest(Aircraft aggregate, CorrectTotalFlightCount command) : SkyhopCommandTest<Aircraft, CorrectTotalFlightCount>(
new CorrectTotalFlightCountHandler(),
aggregate,
command)
{
public class FromCleanSlate() : CorrectTotalFlightCountTest(new Aircraft(), new CorrectTotalFlightCount(0))
{
[Fact]
public void Succeeds() => Result.IsSuccess.Should().BeTrue();

[Fact]
public void HasEvent() => Context.Events.Should().ContainSingle();

[Fact]
public void EventOfType() => Context.Events.Should().ContainItemsAssignableTo<EventEnvelope<TotalFlightCountCorrected>>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using FluentAssertions;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Commands;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Events;
using Whaally.Domain.Event;

namespace Skyhop.Domain.Tests.AircraftContext.Aggregates.Commands;

public abstract class CorrectTotalFlightTimeTest(Aircraft aggregate, CorrectTotalFlightTime command)
: SkyhopCommandTest<Aircraft, CorrectTotalFlightTime>(new CorrectTotalFlightTimeHandler(), aggregate, command)
{
public class FromCleanSlate() : CorrectTotalFlightTimeTest(new Aircraft(), new CorrectTotalFlightTime(TimeSpan.Zero))
{
[Fact]
public void Succeeds() => Result.IsSuccess.Should().BeTrue();

[Fact]
public void HasEvent() => Context.Events.Should().ContainSingle();

[Fact]
public void EventWithType() =>
Context.Events.Should().ContainItemsAssignableTo<EventEnvelope<TotalFlightTimeCorrected>>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using FluentAssertions;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Commands;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate.Events;
using Whaally.Domain.Event;

namespace Skyhop.Domain.Tests.AircraftContext.Aggregates.Commands;

public abstract class RemoveFlightTest(Aircraft aggregate, RemoveFlight command)
: SkyhopCommandTest<Aircraft, RemoveFlight>(new RemoveFlightHandler(), aggregate, command)
{
public class FromCleanSlate() : RemoveFlightTest(new Aircraft(), new RemoveFlight(""))
{
[Fact]
public void Fails() => Result.IsFailed.Should().BeTrue();
}

public class RemoveExistingFlight() : RemoveFlightTest(
new Aircraft
{
Flights = new()
{
{ "1", ( Departure: default, Arrival: default )}
}
},
new RemoveFlight("1"))
{
[Fact]
public void Succeeds() => Result.IsSuccess.Should().BeTrue();

[Fact]
public void WithEvent() => Context.Events.Should().ContainItemsAssignableTo<EventEnvelope<FlightRemoved>>();

[Fact]
public void EventHasFlightId() => ((EventEnvelope<FlightRemoved>)Context.Events.Single())
.Message.FlightId.Should().Be(Command.FlightId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using FluentAssertions;
using Skyhop.Domain.AircraftContext.Aggregates.AircraftAggregate;
using Skyhop.Domain.FlightContext.Aggregates.FlightAggregate;
using Skyhop.Domain.FlightContext.Aggregates.FlightAggregate.Commands;
using Skyhop.Domain.FlightContext.Aggregates.FlightAggregate.Events;
using Skyhop.Domain.FlightContext.Sagas;
using Whaally.Domain;
using Whaally.Domain.Event;

namespace Skyhop.Domain.Tests.FlightContext.Sagas;

/*
* This test works slightly different due to the saga itself calling multiple other sagas.
*
* To properly test we must instantiate the requested aggregates beforehand, which is why we allow tests to define
* the initializer, allowing them to define aggregates.
*/
public abstract class OnAircraftChangedTest(Action<Whaally.Domain.Domain> initializer, EventEnvelope<AircraftSet> @event)
: SkyhopSagaTest<AircraftSet>(initializer, new OnAircraftChanged(), @event)
{
public class FromCleanSlate() : OnAircraftChangedTest(
domain => { },
new(new AircraftSet("aircraft"), new EventMetadata("flight")))
{
[Fact]
public void Succeeds() => Result.IsSuccess.Should().BeTrue();

[Fact(Skip = "Missing the infra to set up this test")]
public void HasSingleCommand() => Context.Commands.Should().ContainSingle();
}

// WIP figuring out how to best test sagas
// public class AddsFlightToAircraft() : OnAircraftChangedTest(
// domain =>
// {
// _ = domain.GetAggregate<Flight>("flight")
// .Result
// .EvaluateAndApply(new Create(), new SetAircraft("aircraft-1"))
// },
// new(new AircraftSet("aircraft-2"), new EventMetadata("flight")))
// {
//
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using Skyhop.Domain.FlightContext.Aggregates.FlightAggregate.Commands;
using Whaally.Domain;

namespace Skyhop.Domain.Tests.FlightContext.Commands;
namespace Skyhop.Domain.Tests.FlightContext.Sagas;

public class OnAircraftChanged_Saga_Tests : DomainTest
{
Expand Down
16 changes: 15 additions & 1 deletion tests/Skyhop.Domain.Tests/SkyhopSagaTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@ namespace Skyhop.Domain.Tests;
public abstract class SkyhopSagaTest<TEvent> : SagaTest<TEvent>
where TEvent : class, IEvent
{
protected SkyhopSagaTest(
public SkyhopSagaTest(
Action<Whaally.Domain.Domain> initializer,
ISaga<TEvent> saga,
IEventEnvelope<TEvent> @event) : base(initializer, saga, @event) { }

public SkyhopSagaTest(
Action<Whaally.Domain.Domain> initializer,
ISaga<TEvent> saga,
TEvent @event) : base(initializer, saga, @event) { }

public SkyhopSagaTest(
ISaga<TEvent> saga,
IEventEnvelope<TEvent> @event) : base(saga, @event) { }

public SkyhopSagaTest(
ISaga<TEvent> saga,
TEvent @event) : base(saga, @event) { }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@ public async Task Should_Create_New_Flight()
var result = await client.PostAsync("/flight/new", null);

Assert.True(result.IsSuccessStatusCode);

}
}
7 changes: 7 additions & 0 deletions tests/Whaally.Domain.Tests/DomainTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ namespace Whaally.Domain.Tests;

public abstract class DomainTest : IDisposable
{
public DomainTest() { }

public DomainTest(Action<Whaally.Domain.Domain>? domainInitializer)
{
domainInitializer?.Invoke(Domain);
}

public IServiceProvider Services = new ServiceCollection()
.AddDomain()
.BuildServiceProvider();
Expand Down
24 changes: 21 additions & 3 deletions tests/Whaally.Domain.Tests/SagaTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
using Whaally.Domain.Abstractions.Command;
using Whaally.Domain.Abstractions.Event;
using Whaally.Domain.Abstractions.Saga;
using Whaally.Domain.Event;
using Whaally.Domain.Saga;

namespace Whaally.Domain.Tests;

// ToDo: Test whether the multiple evaluation of a saga does not meaningfully change the state

public abstract class SagaTest<TEvent> : DomainTest
where TEvent : class, IEvent
{
Expand All @@ -20,13 +23,28 @@ public abstract class SagaTest<TEvent> : DomainTest
public SagaTest(
ISaga<TEvent> saga,
TEvent @event)
: this(default, saga, new EventEnvelope<TEvent>(@event, new EventMetadata(Guid.NewGuid().ToString()))) { }

public SagaTest(
Action<Whaally.Domain.Domain>? initializer,
ISaga<TEvent> saga,
TEvent @event) : this(initializer, saga, new EventEnvelope<TEvent>(@event, new EventMetadata(Guid.NewGuid().ToString()))) { }

public SagaTest(
ISaga<TEvent> saga,
IEventEnvelope<TEvent> @event) : this(default, saga, @event) { }

public SagaTest(
Action<Whaally.Domain.Domain>? initializer,
ISaga<TEvent> saga,
IEventEnvelope<TEvent> @event) : base(initializer)
{
Saga = saga;
Event = @event;
Event = @event.Message;

// ToDo: Provide IServiceProvider
Context = new SagaContext(default!)
Context = new SagaContext(Services)
{
AggregateId = @event.Metadata.AggregateId,
Activity = default
};

Expand Down
4 changes: 1 addition & 3 deletions tests/Whaally.Domain.Tests/ServiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ public ServiceTest(
Handler = handler;
Service = service;

var id = Guid.NewGuid();
// ToDo: Provide IServiceProvider
Context = new ServiceHandlerContext(default!)
Context = new ServiceHandlerContext(Services)
{
Activity = default
};
Expand Down

0 comments on commit ec9f983

Please sign in to comment.