Skip to content

Commit

Permalink
fix(server): on StopAsync, the host was being disposed while host.Sto…
Browse files Browse the repository at this point in the history
…pAsync() was not being awaited. Additionally, lock(object) is not supported in async context so replaced it with SemaphoreSlim. The StartAsync/StopAsync methods now also no longer throw when called multiple times. (#88)
  • Loading branch information
skwasjer authored Nov 18, 2023
1 parent 6e2fdb9 commit 3639e12
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 41 deletions.
59 changes: 31 additions & 28 deletions src/MockHttp.Server/MockHttpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace MockHttp;
public sealed class MockHttpServer : IDisposable
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly object _syncLock = new();
private readonly SemaphoreSlim _lock = new(1);
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly IWebHostBuilder _webHostBuilder;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
Expand Down Expand Up @@ -77,6 +77,8 @@ public MockHttpServer(MockHttpHandler mockHttpHandler, ILoggerFactory? loggerFac
/// <inheritdoc />
public void Dispose()
{
_lock.Dispose();

_host?.Dispose();
_host = null;
}
Expand All @@ -101,72 +103,73 @@ public Uri HostUri
{
get
{
lock (_syncLock)
_lock.Wait();
try
{
string? url = _host?.ServerFeatures.Get<IServerAddressesFeature>()?.Addresses.First();
return url is null
? _hostUri
: new Uri(url);
}
finally
{
_lock.Release();
}
}
}

/// <summary>
/// Gets whether the host is started.
/// </summary>
public bool IsStarted => _host != null;
public bool IsStarted => _host is not null;

/// <summary>
/// Starts listening on the configured addresses.
/// </summary>
public Task StartAsync(CancellationToken cancellationToken = default)
public async Task StartAsync(CancellationToken cancellationToken = default)
{
if (_host != null)
await _lock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
throw new InvalidOperationException($"{nameof(MockHttpServer)} already running.");
}

lock (_syncLock)
{
if (_host != null)
if (_host is not null)
{
return Task.CompletedTask;
return;
}

_host = _webHostBuilder.Build();
return _host.StartAsync(cancellationToken);
await _host.StartAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_lock.Release();
}
}

/// <summary>
/// Attempt to gracefully stop the mock HTTP server.
/// </summary>
public Task StopAsync(CancellationToken cancellationToken = default)
public async Task StopAsync(CancellationToken cancellationToken = default)
{
if (_host == null)
await _lock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
throw new InvalidOperationException($"{nameof(MockHttpServer)} not running.");
}

lock (_syncLock)
{
if (_host == null)
if (_host is null)
{
return Task.CompletedTask;
return;
}

// Make local copy, so we can null it before disposing.
IWebHost host = _host;
_host = null;
try
using (host)
{
return host.StopAsync(cancellationToken);
}
finally
{
host.Dispose();
await host.StopAsync(cancellationToken).ConfigureAwait(false);
}
}
finally
{
_lock.Release();
}
}

/// <summary>
Expand Down
40 changes: 27 additions & 13 deletions test/MockHttp.Server.Tests/MockHttpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,30 +330,44 @@ public void When_creating_server_with_absolute_uri_it_should_not_throw_and_take_
}

[Fact]
public async Task Given_server_is_started_when_starting_again_it_should_throw()
public async Task Given_server_is_started_when_starting_again_it_should_not_throw()
{
using var server = new MockHttpServer(_fixture.Handler, BaseUri);
await server.StartAsync();
server.IsStarted.Should().BeTrue();

// Act
Func<Task> act = () => server.StartAsync();
Func<Task<MockHttpServer>> act = async () =>
{
var server = new MockHttpServer(_fixture.Handler, BaseUri);
await server.StartAsync();
server.IsStarted.Should().BeTrue();
await server.StartAsync();
return server;
};

// Assert
await act.Should().ThrowAsync<InvalidOperationException>();
MockHttpServer? server = (await act.Should().NotThrowAsync()).Which;
using (server)
{
server?.IsStarted.Should().BeTrue();
}
}

[Fact]
public async Task Given_server_is_not_started_when_stopped_it_should_throw()
public async Task Given_server_is_not_started_when_stopped_it_should_not_throw()
{
using var server = new MockHttpServer(_fixture.Handler, BaseUri);
server.IsStarted.Should().BeFalse();

// Act
Func<Task> act = () => server.StopAsync();
Func<Task<MockHttpServer>> act = async () =>
{
var server = new MockHttpServer(_fixture.Handler, BaseUri);
server.IsStarted.Should().BeFalse();
await server.StopAsync();
return server;
};

// Assert
await act.Should().ThrowAsync<InvalidOperationException>();
MockHttpServer? server = (await act.Should().NotThrowAsync()).Which;
using (server)
{
server?.IsStarted.Should().BeFalse();
}
}

[Fact]
Expand Down

0 comments on commit 3639e12

Please sign in to comment.