Skip to content

Commit

Permalink
Added "text file" response writer (#295)
Browse files Browse the repository at this point in the history
* Added "TextFile" option to stub response schema

* Implemented "TextFile" in FileResponseWriter

* Fixed / added tests

* Updated integration test

* Added method to .NET client

* Updated docs

* Updated CHANGELOG

* Updated docs

Co-authored-by: Duco <git@ducode.org>
  • Loading branch information
dukeofharen and Duco committed Nov 30, 2022
1 parent d7fe539 commit 863594b
Show file tree
Hide file tree
Showing 20 changed files with 230 additions and 75 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG
@@ -1,6 +1,7 @@
[vnext]
- Upgraded to .NET 7 (https://github.com/dukeofharen/httplaceholder/pull/293).
- Added "show more" button to request body for when the request body is very long (https://github.com/dukeofharen/httplaceholder/pull/294)
- Added "show more" button to request body for when the request body is very long (https://github.com/dukeofharen/httplaceholder/pull/294).
- Added text file response writer (https://github.com/dukeofharen/httplaceholder/pull/295).

# BREAKING CHANGES
- If you use any of the pre-built binaries of HttPlaceholder, you won't notice anything with upgrading. If you use the .NET tool version of HttPlaceholder, you need to have [.NET 7](https://dotnet.microsoft.com/en-us/) installed from now on.
Expand Down
20 changes: 19 additions & 1 deletion docs/docs.md
Expand Up @@ -1395,6 +1395,25 @@ You can also use the full path to a file.
Content-Type: image/jpeg
```

#### Scenario 3

You can use the `textFile` response writer to enforce HttPlaceholder to read the file as text file. When you use the "text file" respone writer, you can use variable parsers in the text file (read more about it [here](#dynamic-mode)).

```yml
- id: text-file
conditions:
method: GET
url:
path:
equals: /text.txt
response:
statusCode: 200
textFile: C:\files\text.txt
enableDynamicMode: true
headers:
Content-Type: text/plain
```

## Extra duration

Whenever you want to simulate a busy web service, you can use the "extraDuration" response writer. You can set the number of extra milliseconds HttPlaceholder should wait and the request will actually take that much time to complete.
Expand Down Expand Up @@ -2707,7 +2726,6 @@ On the settings page you can configure all kinds of settings for HttPlaceholder
There is a NuGet package available for HttPlaceholder. You can find this client
here: <https://www.nuget.org/packages/HttPlaceholder.Client/>.


### General

This client was built from the ground up. It exposes methods for easily adding the HttPlaceholder client to
Expand Down
Expand Up @@ -23,8 +23,8 @@ public async Task GenerateCurlStubsAsync_SaveStub()
var generator = _mocker.CreateInstance<CurlStubGenerator>();

const string input = "curl commands";
const string expectedStubId1 = "generated-c075229f45159eb65f8334a5a0e93ddf";
const string expectedStubId2 = "generated-5038460ada47d3300c559881289a2777";
const string expectedStubId1 = "generated-9be66c6da831096bc33dd2a341ba75bc";
const string expectedStubId2 = "generated-00d626b467a81f70e505aad67f9bb59c";

var requests = new[] {new HttpRequestModel(), new HttpRequestModel()};
curlToHttpRequestMapperMock
Expand Down Expand Up @@ -75,8 +75,8 @@ public async Task GenerateCurlStubsAsync_DoNotSaveStub()

const string input = "curl commands";
const string tenant = "tenant1";
const string expectedStubId1 = "generated-80d1d38330f9d3c6b646b2c3f017911f";
const string expectedStubId2 = "generated-a0559d5d89721d2ae239d1fbe76408cb";
const string expectedStubId1 = "generated-170e262f39dde6918c05b02cf018cbff";
const string expectedStubId2 = "generated-349817420719656356dec724404e0eda";

var requests = new[] {new HttpRequestModel(), new HttpRequestModel()};
curlToHttpRequestMapperMock
Expand Down
Expand Up @@ -193,8 +193,8 @@ public async Task GenerateHarStubsAsync_HappyFlow_DoNotCreateStub()
stubContextMock.Verify(m => m.DeleteStubAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
stubContextMock.Verify(m => m.AddStubAsync(It.IsAny<StubModel>(), It.IsAny<CancellationToken>()), Times.Never);

Assert.AreEqual("generated-4a14f2ed47bc9ec209d7ea218807f59d", result[0].Stub.Id);
Assert.AreEqual("generated-4a14f2ed47bc9ec209d7ea218807f59d", result[1].Stub.Id);
Assert.AreEqual("generated-4a14f2ed47bc9ec209d7ea218807f59d", result[2].Stub.Id);
Assert.AreEqual("generated-eb1b9a461a7efe703bcd5d61c3b4e7f8", result[0].Stub.Id);
Assert.AreEqual("generated-eb1b9a461a7efe703bcd5d61c3b4e7f8", result[1].Stub.Id);
Assert.AreEqual("generated-eb1b9a461a7efe703bcd5d61c3b4e7f8", result[2].Stub.Id);
}
}
Expand Up @@ -36,7 +36,7 @@ public async Task GenerateStubBasedOnRequestAsync_RequestFound_SaveStub()
var httpRequestToConditionsServiceMock = _mocker.GetMock<IHttpRequestToConditionsService>();
var generator = _mocker.CreateInstance<RequestStubGenerator>();

const string expectedStubId = "generated-faf4a0a7e15cd24a43e87a441815b63b";
const string expectedStubId = "generated-30a6a7a0c897d23e4a3739c6ec120baa";

var request =
new RequestResultModel {CorrelationId = "2", RequestParameters = new RequestParametersModel()};
Expand Down Expand Up @@ -77,7 +77,7 @@ public async Task GenerateStubBasedOnRequestAsync_RequestFound_DoNotSaveStub()
var httpRequestToConditionsServiceMock = _mocker.GetMock<IHttpRequestToConditionsService>();
var generator = _mocker.CreateInstance<RequestStubGenerator>();

const string expectedStubId = "generated-faf4a0a7e15cd24a43e87a441815b63b";
const string expectedStubId = "generated-30a6a7a0c897d23e4a3739c6ec120baa";

var request =
new RequestResultModel {CorrelationId = "2", RequestParameters = new RequestParametersModel()};
Expand Down
Expand Up @@ -94,7 +94,7 @@ public async Task ConvertToStubAsync_HappyFlow()
Assert.AreEqual(stubResponse, result.Response);
Assert.AreEqual(tenant, result.Tenant);
Assert.AreEqual("API to get users", result.Description);
Assert.AreEqual("generated-1de2339d58b51db6b159ae0ba321a56d", result.Id);
Assert.AreEqual("generated-10beced6f421e77a254aec44c1e51d95", result.Id);

Assert.IsNotNull(capturedRequest);
Assert.AreEqual(requestBody, capturedRequest.Body);
Expand Down
Expand Up @@ -21,12 +21,22 @@ public class FileResponseWriterFacts
[TestCleanup]
public void Cleanup() => _mocker.VerifyAll();

[TestMethod]
public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_NoValueSetInStub()
[DataTestMethod]
[DataRow(true)]
[DataRow(false)]
public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_NoValueSetInStub(bool textFile)
{
// Arrange
var writer = _mocker.CreateInstance<FileResponseWriter>();
var stub = new StubModel {Response = new StubResponseModel {File = null}};
var stub = new StubModel {Response = new StubResponseModel()};
if (textFile)
{
stub.Response.TextFile = null;
}
else
{
stub.Response.File = null;
}

var response = new ResponseModel();

Expand All @@ -36,22 +46,34 @@ public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_NoValueSetIn
// Assert
Assert.IsFalse(result.Executed);
Assert.IsNull(response.Body);
Assert.IsFalse(response.BodyIsBinary);
}

[TestMethod]
public async Task FileResponseWriter_WriteToResponseAsync_FileFoundDirectly_NotAllowed()
[DataTestMethod]
[DataRow(true)]
[DataRow(false)]
public async Task FileResponseWriter_WriteToResponseAsync_FileFoundDirectly_NotAllowed(bool textFile)
{
// Arrange
_settings.Stub.AllowGlobalFileSearch = false;
var fileServiceMock = _mocker.GetMock<IFileService>();
var writer = _mocker.CreateInstance<FileResponseWriter>();

var stub = new StubModel {Response = new StubResponseModel {File = @"C:\tmp\image.png"}};
const string path = @"C:\tmp\image.png";
var stub = new StubModel {Response = new StubResponseModel()};
if (textFile)
{
stub.Response.TextFile = path;
}
else
{
stub.Response.File = path;
}

var response = new ResponseModel();

fileServiceMock
.Setup(m => m.FileExistsAsync(stub.Response.File, It.IsAny<CancellationToken>()))
.Setup(m => m.FileExistsAsync(path, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);

// Act
Expand All @@ -62,25 +84,36 @@ public async Task FileResponseWriter_WriteToResponseAsync_FileFoundDirectly_NotA
Assert.AreEqual("Path 'C:\\tmp\\image.png' found, but can't be used because setting 'allowGlobalFileSearch' is turned off. Turn it on with caution. Use paths relative to the .yml stub files or the file storage location as specified in the configuration.", exception.Message);
}

[TestMethod]
public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_FileFoundDirectly()
[DataTestMethod]
[DataRow(true)]
[DataRow(false)]
public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_FileFoundDirectly(bool textFile)
{
// Arrange
_settings.Stub.AllowGlobalFileSearch = true;
var fileServiceMock = _mocker.GetMock<IFileService>();
var writer = _mocker.CreateInstance<FileResponseWriter>();

var body = new byte[] {1, 2, 3};
var stub = new StubModel {Response = new StubResponseModel {File = @"C:\tmp\image.png"}};
const string path = @"C:\tmp\image.png";
var stub = new StubModel {Response = new StubResponseModel()};
if (textFile)
{
stub.Response.TextFile = path;
}
else
{
stub.Response.File = path;
}

var response = new ResponseModel();

fileServiceMock
.Setup(m => m.FileExistsAsync(stub.Response.File, It.IsAny<CancellationToken>()))
.Setup(m => m.FileExistsAsync(path, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);

fileServiceMock
.Setup(m => m.ReadAllBytesAsync(stub.Response.File, It.IsAny<CancellationToken>()))
.Setup(m => m.ReadAllBytesAsync(path, It.IsAny<CancellationToken>()))
.ReturnsAsync(body);

// Act
Expand All @@ -89,13 +122,17 @@ public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_FileFoundDir
// Assert
Assert.IsTrue(result.Executed);
Assert.AreEqual(body, response.Body);
Assert.AreEqual(response.BodyIsBinary, !textFile);
}

[DataTestMethod]
[DataRow("image.png", "image.png")]
[DataRow("../image.png", "image.png")]
[DataRow("../../image.png", "image.png")]
public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_FileNotFoundDirectly_ButFoundInStubFolder(string file, string actualFile)
[DataRow(true, "image.png", "image.png")]
[DataRow(true, "../image.png", "image.png")]
[DataRow(true, "../../image.png", "image.png")]
[DataRow(false, "image.png", "image.png")]
[DataRow(false, "../image.png", "image.png")]
[DataRow(false, "../../image.png", "image.png")]
public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_FileNotFoundDirectly_ButFoundInStubFolder(bool textFile, string file, string actualFile)
{
// Arrange
var stubRootPathResolverMock = _mocker.GetMock<IStubRootPathResolver>();
Expand All @@ -105,7 +142,15 @@ public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_FileNotFound
var stubRootPaths = new[] {"/var/stubs1", "/var/stubs2"};
var expectedPath = Path.Combine(stubRootPaths[1], actualFile);
var body = new byte[] {1, 2, 3};
var stub = new StubModel {Response = new StubResponseModel {File = file}};
var stub = new StubModel {Response = new StubResponseModel()};
if (textFile)
{
stub.Response.TextFile = file;
}
else
{
stub.Response.File = file;
}

var response = new ResponseModel();

Expand All @@ -131,6 +176,7 @@ public async Task FileResponseWriter_WriteToResponseAsync_HappyFlow_FileNotFound
// Assert
Assert.IsTrue(result.Executed);
Assert.AreEqual(body, response.Body);
Assert.AreEqual(response.BodyIsBinary, !textFile);
}

[TestMethod]
Expand Down Expand Up @@ -167,5 +213,6 @@ public async Task
// Assert
Assert.IsFalse(result.Executed);
Assert.IsNull(response.Body);
Assert.IsFalse(response.BodyIsBinary);
}
}
Expand Up @@ -35,6 +35,6 @@ public void EnsureStubId_StubIdNotSet_ShouldSetStubId()

// Assert
Assert.AreEqual(stub.Id, result);
Assert.AreEqual("generated-2f06ac203bd36135ac352ce504c1a256", result);
Assert.AreEqual("generated-055b2e3db5303ab3279025e78e48db3c", result);
}
}
Expand Up @@ -43,15 +43,16 @@ internal class FileResponseWriter : IResponseWriter, ISingletonService
CancellationToken cancellationToken)
{
var settings = _options.CurrentValue;
if (stub.Response?.File == null)
if (stub.Response?.File == null && stub.Response?.TextFile == null)
{
return StubResponseWriterResultModel.IsNotExecuted(GetType().Name);
}

var file = stub.Response?.File ?? stub.Response?.TextFile;
string finalFilePath = null;
if (await _fileService.FileExistsAsync(stub.Response.File, cancellationToken))
if (await _fileService.FileExistsAsync(file, cancellationToken))
{
finalFilePath = stub.Response.File;
finalFilePath = file;
if (settings.Stub?.AllowGlobalFileSearch == false)
{
throw new InvalidOperationException(
Expand All @@ -66,7 +67,7 @@ internal class FileResponseWriter : IResponseWriter, ISingletonService
var stubRootPaths = await _stubRootPathResolver.GetStubRootPathsAsync(cancellationToken);
foreach (var path in stubRootPaths)
{
var tempPath = Path.Combine(path, PathUtilities.CleanPath(stub.Response.File));
var tempPath = Path.Combine(path, PathUtilities.CleanPath(file));
if (!await _fileService.FileExistsAsync(tempPath, cancellationToken))
{
_logger.LogInformation($"Path '{tempPath}' not found.");
Expand All @@ -85,8 +86,7 @@ internal class FileResponseWriter : IResponseWriter, ISingletonService
}

response.Body = await _fileService.ReadAllBytesAsync(finalFilePath, cancellationToken);
response.BodyIsBinary = true;

response.BodyIsBinary = string.IsNullOrWhiteSpace(stub.Response.TextFile);
return StubResponseWriterResultModel.IsExecuted(GetType().Name);
}
}
Expand Up @@ -140,6 +140,18 @@ public void WithFile()
Assert.AreEqual("/var/file.jpg", response.File);
}

[TestMethod]
public void WithTextFile()
{
// Act
var response = StubResponseBuilder.Begin()
.WithTextFile("/var/file.txt")
.Build();

// Assert
Assert.AreEqual("/var/file.txt", response.TextFile);
}

[TestMethod]
public void WithResponseHeader()
{
Expand Down
5 changes: 5 additions & 0 deletions src/HttPlaceholder.Client/Dto/Stubs/StubResponseDto.cs
Expand Up @@ -38,6 +38,11 @@ public class StubResponseDto
/// </summary>
public string File { get; set; }

/// <summary>
/// Gets or sets the text file.
/// </summary>
public string TextFile { get; set; }

/// <summary>
/// Gets or sets the headers.
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/HttPlaceholder.Client/StubBuilders/StubResponseBuilder.cs
Expand Up @@ -145,6 +145,17 @@ public StubResponseBuilder WithFile(string filePath)
return this;
}

/// <summary>
/// Sets a text file path to return to the response definition.
/// </summary>
/// <param name="filePath">The text file path of the file to return.</param>
/// <returns>The current <see cref="StubResponseBuilder" />.</returns>
public StubResponseBuilder WithTextFile(string filePath)
{
_response.TextFile = filePath;
return this;
}

/// <summary>
/// Sets a response header to be returned to the response definition.
/// This method can be called multiple times to add multiple response headers to be returned.
Expand Down
6 changes: 6 additions & 0 deletions src/HttPlaceholder.Domain/StubResponseModel.cs
Expand Up @@ -47,6 +47,12 @@ public class StubResponseModel
[YamlMember(Alias = "file")]
public string File { get; set; }

/// <summary>
/// Gets or sets the text file.
/// </summary>
[YamlMember(Alias = "textFile")]
public string TextFile { get; set; }

/// <summary>
/// Gets or sets the headers.
/// </summary>
Expand Down

0 comments on commit 863594b

Please sign in to comment.