Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
<PackageVersion Include="Cratis.Screenplay.Generation" Version="$(CratisScreenplayGenerationVersion)" />
<PackageVersion Include="Cratis.Screenplay.Generation.DotNet" Version="$(CratisScreenplayGenerationVersion)" />
<PackageVersion Include="Cratis.Screenplay.Generation.DotNet.Vogen" Version="$(CratisScreenplayGenerationVersion)" />
<PackageVersion Include="Cratis.Chronicle.Connections" Version="18.1.0" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="18.1.0" />
<PackageVersion Include="Cratis.Chronicle.Connections" Version="19.6.1" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="19.6.1" />
<PackageVersion Include="Cratis.Fundamentals" Version="7.19.2" />
<!-- Prologue -->
<PackageVersion Include="Cratis.Prologue.Configuration" Version="2.0.0" />
Expand Down Expand Up @@ -68,7 +68,7 @@
<!-- Testing -->
<PackageVersion Include="Cratis.Specifications" Version="4.1.0" />
<PackageVersion Include="Cratis.Specifications.XUnit" Version="4.1.0" />
<PackageVersion Include="Cratis.Chronicle.XUnit.Integration" Version="18.1.0" />
<PackageVersion Include="Cratis.Chronicle.XUnit.Integration" Version="19.6.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.10.0" />
Expand Down
16 changes: 13 additions & 3 deletions Integration/Chronicle/for_Observers/when_retrying_partition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

namespace Cratis.Cli.Integration.Chronicle.for_Observers;

/// <summary>
/// Against a real observer with no failed partition of the given key, this is what #186 was about: the command
/// used to await the call, ignore what came back, and print "Retry started" regardless. The key here was never a
/// real failure, so the kernel declines - and the point of the fix is that the command now says so instead of
/// claiming success for a retry that never started.
/// </summary>
/// <param name="context">The <see cref="context"/> this specification runs in.</param>
[Collection(ChronicleCollection.Name)]
public class when_retrying_partition(context context) : CliGiven<context>(context)
{
Expand All @@ -23,9 +30,12 @@ async Task Because()
}
}

[Fact] void should_return_success_exit_code() => Context.Result.ExitCode.ShouldEqual(ExitCodes.Success);
[Fact] void should_not_return_success_exit_code() => Context.Result.ExitCode.ShouldNotEqual(ExitCodes.Success);

[Fact] void should_contain_retry_started_message() => Context.Result.StandardOutput.ShouldContain("Retry started");
[Fact] void should_return_a_validation_error_exit_code() => Context.Result.ExitCode.ShouldEqual(ExitCodes.ValidationError);

[Fact] void should_have_no_errors() => Context.Result.StandardError.ShouldEqual(string.Empty);
[Fact] void should_not_claim_a_retry_started() => Context.Result.StandardOutput.ShouldNotContain("Retry started");

[Fact] void should_say_the_partition_was_not_among_the_failures() =>
Context.Result.StandardError.ShouldContain("is not among the failed partitions");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Contracts;
using Cratis.Chronicle.Contracts.Observation;
using Cratis.Cli.Commands.Chronicle.Observers;
using ClearObserverQuarantineContract = Cratis.Chronicle.Contracts.Observation.ClearObserverQuarantine;

namespace Cratis.Cli.for_ClearObserverQuarantineCommand.when_clearing_quarantine;

/// <summary>
/// This replaces a specification that passed for the whole life of issue #185, while the command failed against
/// every kernel it was pointed at. The command resolved its contract method by reflection, which cannot see
/// explicitly implemented members, and the shipped client implements the contract explicitly - but the double it
/// was specified against implemented it implicitly, so the lookup succeeded in the specification and nowhere else.
/// Running against <see cref="given.an_observers_client_shaped_like_the_generated_proxy"/> is what closes that gap:
/// the double now fails the way the real client does.
/// </summary>
public class and_the_kernel_accepts_it : Specification
{
IObservers _observers;
IServices _services;
ObserverCommandSettings _settings;
int _result;
ClearObserverQuarantineContract _sent;

void Establish()
{
_observers = Substitute.For<IObservers>();
_services = Substitute.For<IServices>();
_services.Observers.Returns(new given.an_observers_client_shaped_like_the_generated_proxy(_observers));

_settings = new ObserverCommandSettings
{
EventStore = "the-event-store",
Namespace = "the-namespace",
ObserverId = "the-observer",
EventSequenceId = "event-log"
};
}

async Task Because()
{
_result = await new ClearObserverQuarantineCommandForSpecs().Execute(_services, _settings);
_sent = _observers.ReceivedCalls()
.Select(call => call.GetArguments()[0])
.OfType<ClearObserverQuarantineContract>()
.Single();
}

[Fact] void should_succeed() => _result.ShouldEqual(ExitCodes.Success);
[Fact] void should_clear_the_observer_it_was_given() => _sent.ObserverId.ShouldEqual("the-observer");
[Fact] void should_target_the_resolved_event_store() => _sent.EventStore.ShouldEqual("the-event-store");
[Fact] void should_target_the_resolved_namespace() => _sent.Namespace.ShouldEqual("the-namespace");
[Fact] void should_pass_the_event_sequence() => _sent.EventSequenceId.ShouldEqual("event-log");

class ClearObserverQuarantineCommandForSpecs : ClearObserverQuarantineCommand
{
public Task<int> Execute(IServices services, ObserverCommandSettings settings) =>
ExecuteCommandAsync(services, settings, "json");
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Contracts;
using Cratis.Chronicle.Contracts.Observation;
using Cratis.Cli.Commands.Chronicle.Observers;
using Cratis.Cli.given;
using RemoveObserverContract = Cratis.Chronicle.Contracts.Observation.RemoveObserver;

namespace Cratis.Cli.for_RemoveObserverCommand.given;

public class a_remove_observer_command : Specification
{
protected IServices _services;
protected IObservers _observers;
protected ObserverCommandSettings _settings;
protected RemoveObserverCommandForSpecs _command;

void Establish()
{
_observers = Substitute.For<IObservers>();
_services = Substitute.For<IServices>();

// Explicitly implemented, like the shipped client - see the double's own remarks and issue #185.
_services.Observers.Returns(new an_observers_client_shaped_like_the_generated_proxy(_observers));

_settings = new ObserverCommandSettings
{
EventStore = "the-event-store",
Namespace = "the-namespace",
ObserverId = "the-observer",
EventSequenceId = "event-log"
};

_command = new RemoveObserverCommandForSpecs();
Removal(ObserverRemovalOutcome.Removed);
}

protected void Removal(ObserverRemovalOutcome outcome, string blockingNamespace = "") =>
_observers
.RemoveObserver(Arg.Any<RemoveObserverContract>(), Arg.Any<ProtoBuf.Grpc.CallContext>())
.Returns(new RemoveObserverResponse { Outcome = outcome, BlockingNamespace = blockingNamespace });

protected Task<int> Execute() => _command.Execute(_services, _settings);

/// <summary>
/// Exposes the command's execution and its confirmation prompt, both of which are protected on the base command.
/// </summary>
public class RemoveObserverCommandForSpecs : RemoveObserverCommand
{
public Task<int> Execute(IServices services, ObserverCommandSettings settings) =>
ExecuteCommandAsync(services, settings, "json");

public string Confirmation(ObserverCommandSettings settings) => GetConfirmationPrompt(settings);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_RemoveObserverCommand.when_confirming;

/// <summary>
/// Removal cannot be undone and reaches every namespace in the event store, so the prompt has to say what goes, what
/// stays and that a re-registered observer starts over - the distinction from a replay, which is what an operator
/// reaching for this may well have meant. A bare "are you sure?" gives no basis for answering.
/// </summary>
public class and_the_operator_is_asked : given.a_remove_observer_command
{
string _prompt;

void Because() => _prompt = _command.Confirmation(_settings);

[Fact] void should_name_the_observer() => _prompt.ShouldContain("the-observer");
[Fact] void should_name_the_event_store() => _prompt.ShouldContain("the-event-store");
[Fact] void should_say_it_covers_every_namespace() => _prompt.ShouldContain("every namespace");
[Fact] void should_say_read_models_are_left_alone() => _prompt.ShouldContain("Read models and their data are not touched");
[Fact] void should_say_it_cannot_be_undone() => _prompt.ShouldContain("cannot be undone");
[Fact] void should_say_a_re_registered_observer_starts_over() => _prompt.ShouldContain("start over");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Contracts.Observation;

namespace Cratis.Cli.for_RemoveObserverCommand.when_removing;

public class and_the_observer_does_not_exist : given.a_remove_observer_command
{
int _result;

void Establish() => Removal(ObserverRemovalOutcome.ObserverNotFound);

async Task Because() => _result = await Execute();

[Fact] void should_report_it_as_not_found() => _result.ShouldEqual(ExitCodes.NotFound);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using RemoveObserverContract = Cratis.Chronicle.Contracts.Observation.RemoveObserver;

namespace Cratis.Cli.for_RemoveObserverCommand.when_removing;

public class and_the_observer_is_removed : given.a_remove_observer_command
{
int _result;
RemoveObserverContract _sent;

async Task Because()
{
_result = await Execute();
_sent = _observers.ReceivedCalls()
.Select(call => call.GetArguments()[0])
.OfType<RemoveObserverContract>()
.Single();
}

[Fact] void should_succeed() => _result.ShouldEqual(ExitCodes.Success);
[Fact] void should_remove_the_observer_it_was_given() => _sent.ObserverId.ShouldEqual("the-observer");
[Fact] void should_target_the_resolved_event_store() => _sent.EventStore.ShouldEqual("the-event-store");
[Fact] void should_target_the_resolved_namespace() => _sent.Namespace.ShouldEqual("the-namespace");
[Fact] void should_pass_the_event_sequence() => _sent.EventSequenceId.ShouldEqual("event-log");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Contracts.Observation;

namespace Cratis.Cli.for_RemoveObserverCommand.when_removing;

/// <summary>
/// A refusal has to exit non-zero. It comes back as a perfectly successful RPC that declined, so reporting the
/// outcome is the only thing that separates "the observer is gone" from "nothing happened" - and a script that only
/// looks at the exit code would otherwise carry on as though the store had been cleaned up.
/// </summary>
public class and_the_observer_is_still_running : given.a_remove_observer_command
{
int _result;

void Establish() => Removal(ObserverRemovalOutcome.ObserverActive, "the-busy-namespace");

async Task Because() => _result = await Execute();

[Fact] void should_not_report_success() => _result.ShouldNotEqual(ExitCodes.Success);
[Fact] void should_report_a_validation_error() => _result.ShouldEqual(ExitCodes.ValidationError);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Contracts;
using Cratis.Chronicle.Contracts.Observation;
using Cratis.Cli.Commands.Chronicle.Observers;
using Cratis.Cli.given;
using RetryPartitionContract = Cratis.Chronicle.Contracts.Observation.RetryPartition;

namespace Cratis.Cli.for_RetryPartitionCommand.given;

public class a_retry_partition_command : Specification
{
protected IServices _services;
protected IObservers _observers;
protected PartitionCommandSettings _settings;
protected RetryPartitionCommandForSpecs _command;

void Establish()
{
_observers = Substitute.For<IObservers>();
_services = Substitute.For<IServices>();

// Explicitly implemented, like the shipped client - see the double's own remarks and issue #185.
_services.Observers.Returns(new an_observers_client_shaped_like_the_generated_proxy(_observers));

_settings = new PartitionCommandSettings
{
EventStore = "the-event-store",
Namespace = "the-namespace",
ObserverId = "the-observer",
Partition = "the-partition",
EventSequenceId = "event-log"
};

_command = new RetryPartitionCommandForSpecs();
Recovery(PartitionRecoveryOutcome.Started);
}

protected void Recovery(PartitionRecoveryOutcome outcome) =>
_observers
.RetryPartition(Arg.Any<RetryPartitionContract>(), Arg.Any<ProtoBuf.Grpc.CallContext>())
.Returns(new RetryPartitionResponse { Outcome = outcome });

protected Task<int> Execute() => _command.Execute(_services, _settings);

public class RetryPartitionCommandForSpecs : RetryPartitionCommand
{
public Task<int> Execute(IServices services, PartitionCommandSettings settings) =>
ExecuteCommandAsync(services, settings, "json");
}
}
Loading
Loading