Skip to content

Commit

Permalink
Add support for response sequences
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Morris-Hill committed Oct 18, 2023
1 parent d0e5a4c commit 90625d5
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 30 deletions.
99 changes: 75 additions & 24 deletions FluentSim/FluentConfigurator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
Expand All @@ -8,25 +9,50 @@

namespace FluentSim
{
public class FluentConfigurator : RouteConfigurer
[DebuggerDisplay("{Description}")]
public class DefinedResponse
{
public string Output = "";
private string Description { get; set; }
public void AddDescriptionPart(string part) => Description += " " + part;
public byte[] BinaryOutput = null;
public bool ShouldImmediatelyDisconnect = false;
public List<Action<HttpListenerContext>> ResponseModifiers = new List<Action<HttpListenerContext>>();

internal string GetBody()
{
return Output;
}

internal void RunContextModifiers(HttpListenerContext context)
{
foreach (var responseModifier in ResponseModifiers)
responseModifier(context);
}
}

public class FluentConfigurator : RouteConfigurer, RouteSequenceConfigurer
{
private string Output = "";
private string Path;
private HttpVerb HttpVerb;
private ManualResetEventSlim RespondToRequests = new ManualResetEventSlim(true);
private TimeSpan RouteDelay;
private List<Action<HttpListenerContext>> ResponseModifiers = new List<Action<HttpListenerContext>>();
private JsonSerializerSettings JsonSerializerSettings;
private List<ReceivedRequest> ReceivedRequests = new List<ReceivedRequest>();
public byte[] BinaryOutput = null;
public Dictionary<string, string> QueryParameters = new Dictionary<string, string>();
public bool ShouldImmediatelyDisconnect = false;
private DefinedResponse CurrentResponse = new DefinedResponse();
private int NextResponseIndex = 0;
private List<DefinedResponse> Responses;

public FluentConfigurator(string path, HttpVerb get, JsonSerializerSettings jsonConverter)
{
JsonSerializerSettings = jsonConverter;
HttpVerb = get;
Path = path;
Responses = new List<DefinedResponse>
{
CurrentResponse
};
}

public HttpMethod Method { get; set; }
Expand All @@ -39,25 +65,28 @@ public void AddReceivedRequest(ReceivedRequest request)

public RouteConfigurer Responds<T>(T output)
{
Output = JsonConvert.SerializeObject(output, JsonSerializerSettings);
CurrentResponse.Output = JsonConvert.SerializeObject(output, JsonSerializerSettings);
return this;
}

public RouteConfigurer Responds(byte[] output)
{
BinaryOutput = output;
CurrentResponse.AddDescriptionPart("With binary output");
CurrentResponse.BinaryOutput = output;
return this;
}

public RouteConfigurer WithCode(int code)
{
ResponseModifiers.Add(ctx => ctx.Response.StatusCode = code);
CurrentResponse.AddDescriptionPart("With code " + code);
CurrentResponse.ResponseModifiers.Add(ctx => ctx.Response.StatusCode = code);
return this;
}

public RouteConfigurer WithHeader(string headerName, string headerValue)
{
ResponseModifiers.Add(ctx => ctx.Response.AddHeader(headerName, headerValue));
CurrentResponse.AddDescriptionPart("With header " + headerName + " = " + headerValue);
CurrentResponse.ResponseModifiers.Add(ctx => ctx.Response.AddHeader(headerName, headerValue));
return this;
}

Expand Down Expand Up @@ -87,7 +116,7 @@ public RouteConfigurer Resume()

public RouteConfigurer Responds(string output)
{
Output = output;
CurrentResponse.Output = output;
return this;
}

Expand All @@ -97,11 +126,6 @@ public RouteConfigurer MatchingRegex()
return this;
}

internal string GetBody()
{
return Output;
}

internal bool DoesRouteMatch(HttpListenerRequest contextRequest)
{
if (!Path.EndsWith("/") && !IsRegex) Path += "/";
Expand Down Expand Up @@ -139,20 +163,15 @@ internal void WaitUntilReadyToRespond()
RespondToRequests.Wait();
}

internal void RunContextModifiers(HttpListenerContext context)
{
foreach (var responseModifier in ResponseModifiers)
responseModifier(context);
}

public RouteConfigurer Responds()
{
return this;
}

public RouteConfigurer WithCookie(Cookie cookie)
{
ResponseModifiers.Add(ctx => ctx.Response.SetCookie(cookie));
CurrentResponse.AddDescriptionPart("With cookie " + cookie.Name + " = " + cookie.Value);
CurrentResponse.ResponseModifiers.Add(ctx => ctx.Response.SetCookie(cookie));
return this;
}

Expand All @@ -161,9 +180,32 @@ public IRouteHistory History()
return new RouteHistory(ReceivedRequests.AsReadOnly());
}

public void ImmediatelyAborts()
public RouteConfigurer ImmediatelyAborts()
{
CurrentResponse.ShouldImmediatelyDisconnect = true;
return this;
}

public void ResetCurrentResponseIndex()
{
NextResponseIndex = 0;
}

public RouteSequenceConfigurer ThenResponds(string bodyText)
{
ShouldImmediatelyDisconnect = true;
CurrentResponse = new DefinedResponse
{
Output = bodyText
};
Responses.Add(CurrentResponse);
return this;
}


public RouteSequenceConfigurer ThenResponds()
{
ThenResponds("");
return this;
}

private class RouteHistory : IRouteHistory
Expand All @@ -174,6 +216,15 @@ public RouteHistory(IReadOnlyList<ReceivedRequest> requests)
}
public IReadOnlyList<ReceivedRequest> ReceivedRequests { get; }
}

public DefinedResponse GetNextDefinedResponse()
{
if(NextResponseIndex >= Responses.Count)
return Responses[Responses.Count - 1];
var resp = Responses[NextResponseIndex];
NextResponseIndex++;
return resp;
}
}

}
9 changes: 5 additions & 4 deletions FluentSim/FluentSimulator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ private void TryToProcessRequest(IAsyncResult ar)
BeginGetContext();
return;
}
var definedResponse = matchingRoute.GetNextDefinedResponse();

if (matchingRoute.ShouldImmediatelyDisconnect)
if (definedResponse.ShouldImmediatelyDisconnect)
{
response.Abort();
return;
Expand All @@ -96,12 +97,12 @@ private void TryToProcessRequest(IAsyncResult ar)
matchingRoute.AddReceivedRequest(receivedRequest);
matchingRoute.WaitUntilReadyToRespond();

matchingRoute.RunContextModifiers(context);
definedResponse.RunContextModifiers(context);

byte[] buffer = matchingRoute.BinaryOutput;
byte[] buffer = definedResponse.BinaryOutput;

if(buffer == null)
buffer = Encoding.UTF8.GetBytes(matchingRoute.GetBody());
buffer = Encoding.UTF8.GetBytes(definedResponse.GetBody());

response.ContentLength64 = buffer.Length;
var output = response.OutputStream;
Expand Down
9 changes: 8 additions & 1 deletion FluentSim/RouteConfigurer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ public interface RouteConfigurer
RouteConfigurer Resume();
RouteConfigurer WithCookie(Cookie cookie);
IRouteHistory History();
void ImmediatelyAborts();
RouteConfigurer ImmediatelyAborts();
RouteSequenceConfigurer ThenResponds(string bodyText);
RouteSequenceConfigurer ThenResponds();
}

public interface RouteSequenceConfigurer : RouteConfigurer
{
void ResetCurrentResponseIndex();
}
}
53 changes: 52 additions & 1 deletion FluentSimTests/FluentSimTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,58 @@ public void CanRespondWithCodes()
MakePostRequest("/test", "").StatusCode.ShouldEqual(HttpStatusCode.Ambiguous);
}

[Test]
public void CanCreateASequenceOfResponses()
{
Sim.Post("/test")
.Responds().WithCode(400)
.ThenResponds()
.WithCode(500)
.ThenResponds()
.WithCode(200);

MakePostRequest("/test", "").StatusCode.ShouldEqual(HttpStatusCode.BadRequest);
MakePostRequest("/test", "").StatusCode.ShouldEqual(HttpStatusCode.InternalServerError);
MakePostRequest("/test", "").StatusCode.ShouldEqual(HttpStatusCode.OK);
}

[Test]
public void GivenASequenceOfEventsItReusesTheLastResponse()
{
Sim.Post("/test")
.Responds().WithCode(400)
.ThenResponds()
.WithCode(500);

MakePostRequest("/test", "").StatusCode.ShouldEqual(HttpStatusCode.BadRequest);
MakePostRequest("/test", "").StatusCode.ShouldEqual(HttpStatusCode.InternalServerError);
MakePostRequest("/test", "").StatusCode.ShouldEqual(HttpStatusCode.InternalServerError);
MakePostRequest("/test", "").StatusCode.ShouldEqual(HttpStatusCode.InternalServerError);
}

[Test]
public void CanOutputASequenceOfDifferentBodies()
{
Sim.Post("/test")
.Responds("first")
.ThenResponds("second");

MakePostRequest("/test", "").Content.ShouldEqual("first");
MakePostRequest("/test", "").Content.ShouldEqual("second");
}

[Test]
public void CanResetRouteSequenceCount()
{
var route = Sim.Post("/test")
.Responds("first")
.ThenResponds("second");

MakePostRequest("/test", "").Content.ShouldEqual("first");
route.ResetCurrentResponseIndex();
MakePostRequest("/test", "").Content.ShouldEqual("first");
}

[Test]
public void CanRespondWithHeaders()
{
Expand All @@ -203,7 +255,6 @@ public void CanRespondWithCookies()
[Test]
public void CanImmediatelyAbortConnection()
{

Sim.Get("/test").ImmediatelyAborts();
var resp = MakeGetRequest("/test", 100);
resp.StatusCode.ShouldEqual((HttpStatusCode)0);
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,51 @@ You can tell the API to abort the connection (interally uses `HttpListenerRespon
simulator.Post("/authenticate").ImmediatelyAborts();
```

## Response sequences
Sometimes you need to test that your code is able to gracefully handle a sequence of bad responses and retry successfully.
Routes can be configured to return a sequence of different responses.

```c#
var route = simulator.Get("/employee/1")
.Responds("John Smith")
.ThenResponds("Jane Doe")
.ThenResponds("Bob Jones");
```

```c#
var route = simulator.Get("/employee/1")
.Responds().WithCode(500)
.ThenResponds().WithCode(429)
.ThenResponds().WithCode(429)
.ThenResponds().WithCode(200);
```

During your tests you can reset the response index if you need to by calling `ResetCurrentResponseIndex()` on the returned route from the `ThenResponds` call.

```c#
var route = Sim.Post("/test")
.Responds("first")
.ThenResponds("second");

MakePostRequest("/test").Content.ShouldEqual("first");
route.ResetCurrentResponseIndex();
// This would have returned "second" if we hadn't reset the index
MakePostRequest("/test").Content.ShouldEqual("first");
```

At the end of the sequence the last response will be used for any future requests.

```c#
var route = Sim.Post("/test")
.Responds("first")
.ThenResponds("second");

// First call returns "first"
// Second call returns "second"
// Third call returns "second" ... and so on
```

## Indefinitely suspend responses at runtime
You can check that your webpage correctly displays loading messages or spinners.

Expand Down

0 comments on commit 90625d5

Please sign in to comment.