Skip to content

Commit

Permalink
Feature/298 multiple methods (#299)
Browse files Browse the repository at this point in the history
* Added method array to API

* Migrated Method to object type

* Added new method to ConversionUtilities

* Updated sample

* Updated MethodConditionChecker

* Added integration tests

* Updated Postman integration test

* Added builder method to .NET client

* Added HTTP method array to UI

* Updated docs

* Updated CHANGELOG

Co-authored-by: Duco <git@ducode.org>
  • Loading branch information
dukeofharen and Duco authored Jan 17, 2023
1 parent 4739056 commit 635a820
Show file tree
Hide file tree
Showing 21 changed files with 396 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Added text file response writer (https://github.com/dukeofharen/httplaceholder/pull/295).
- Updated request body variable parser to also support regular expressions to insert part of request body in the response body (https://github.com/dukeofharen/httplaceholder/pull/296).
- An option was added to user the display URL variable parser with a regular expression, to parse only a part of the URL as part of your response (https://github.com/dukeofharen/httplaceholder/pull/297).
- Updated the HTTP method condition checker so it is now possible to provide multiple HTTP methods if needed (https://github.com/dukeofharen/httplaceholder/issues/298).

# 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
13 changes: 13 additions & 0 deletions docs/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,19 @@ This condition checker can check the HTTP method (e.g. GET, POST, PUT, DELETE et
text: OK
```

It is also possible to specify multiple HTTP methods. A request with any of these HTTP requests will then succeed.

```yml
- id: situation-01
conditions:
method:
- GET
- POST
response:
statusCode: 200
text: OK
```

**Correct request**
- Method: GET
- URL: http://localhost:5000/anyPath
Expand Down
14 changes: 13 additions & 1 deletion docs/samples/01-get.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,16 @@
response:
text: 😻
headers:
Content-Type: text/plain; charset=utf8
Content-Type: text/plain; charset=utf8

# You can also provide multiple methods, so your stub can react to any of the provided HTTP methods.
- id: multiple-methods
conditions:
url:
path:
equals: /multiple-methods
method:
- GET
- POST
response:
text: OK GET or POST
13 changes: 12 additions & 1 deletion gui/src/components/stubForm/formHelper/HttpMethodSelector.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<template>
<div class="list-group">
<button
class="list-group-item list-group-item-action fw-bold"
@click="multipleMethods"
>
Multiple methods
</button>
<button
v-for="(method, index) of httpMethods"
:key="index"
Expand All @@ -15,6 +21,7 @@
import { useStubFormStore } from "@/store/stubForm";
import { defineComponent } from "vue";
import { httpMethods } from "@/domain/stubForm/http-methods";
import { defaultValues } from "@/domain/stubForm/default-values";
export default defineComponent({
name: "HttpMethodSelector",
Expand All @@ -26,8 +33,12 @@ export default defineComponent({
stubFormStore.setMethod(method);
stubFormStore.closeFormHelper();
};
const multipleMethods = () => {
stubFormStore.setMethod(defaultValues.methods);
stubFormStore.closeFormHelper();
};
return { httpMethods, methodSelected };
return { httpMethods, methodSelected, multipleMethods };
},
});
</script>
Expand Down
2 changes: 1 addition & 1 deletion gui/src/domain/stub/stub-conditions-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { StubBasicAuthenticationModel } from "@/domain/stub/stub-basic-auth
import type { StubConditionsScenarioModel } from "@/domain/stub/stub-conditions-scenario-model";

export interface StubConditionsModel {
method?: string;
method?: string | string[];
url?: StubUrlConditionModel;
body?: any[];
form?: StubFormModel[];
Expand Down
1 change: 1 addition & 0 deletions gui/src/domain/stubForm/default-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ResponseImageType } from "@/domain/stub/enums/response-image-type";

export const defaultValues = {
description: "A description for the stub.",
methods: ["GET", "POST"],
priority: 1,
urlPath: "/path",
fullPath: "/path?query=val1",
Expand Down
2 changes: 1 addition & 1 deletion gui/src/domain/stubForm/element-descriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const elementDescriptions = {
disable:
"By setting 'enabled' to false, the stub will not be used when determining which stub should be executed for a request.",
httpMethod:
"This condition checker can check the HTTP method (e.g. GET, POST, PUT, DELETE etc.).",
"This condition checker can check the HTTP method (e.g. GET, POST, PUT, DELETE etc.). You can also provide an array of multiple HTTP methods; a request with any of these HTTP methods will then succeed.",
urlPath:
"The path condition is used to check a part of the URL path (so the part after http://... and before the query string). The condition can both check on substring and regular expressions.",
queryString:
Expand Down
2 changes: 1 addition & 1 deletion gui/src/store/stubForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export const useStubFormStore = defineStore({
}
});
},
setMethod(method: string): void {
setMethod(method: string | string[]): void {
handle(() => {
const parsed = parseInput(this.input);
if (parsed) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using HttPlaceholder.Application.Interfaces.Http;
using System.Collections.Generic;
using HttPlaceholder.Application.Interfaces.Http;
using HttPlaceholder.Application.StubExecution.ConditionCheckers;
using HttPlaceholder.Domain.Enums;

Expand All @@ -7,64 +8,90 @@ namespace HttPlaceholder.Application.Tests.StubExecution.ConditionCheckers;
[TestClass]
public class MethodConditionCheckerFacts
{
private readonly Mock<IHttpContextService> _httpContextServiceMock = new();
private MethodConditionChecker _checker;

[TestInitialize]
public void Initialize() =>
_checker = new MethodConditionChecker(
_httpContextServiceMock.Object);
private readonly AutoMocker _mocker = new();

[TestCleanup]
public void Cleanup() => _httpContextServiceMock.VerifyAll();
public void Cleanup() => _mocker.VerifyAll();

[TestMethod]
public async Task MethodConditionChecker_ValidateAsync_StubsFound_ButNoMethodConditions_ShouldReturnNotExecuted()
{
// arrange
// Arrange
var conditions = new StubConditionsModel {Method = null};
var checker = _mocker.CreateInstance<MethodConditionChecker>();

// act
// Act
var result =
await _checker.ValidateAsync(new StubModel {Id = "id", Conditions = conditions}, CancellationToken.None);
await checker.ValidateAsync(new StubModel {Id = "id", Conditions = conditions}, CancellationToken.None);

// assert
// Assert
Assert.AreEqual(ConditionValidationType.NotExecuted, result.ConditionValidation);
}

[TestMethod]
public async Task MethodConditionChecker_ValidateAsync_StubsFound_WrongMethod_ShouldReturnInvalid()
{
// arrange
// Arrange
var conditions = new StubConditionsModel {Method = "POST"};
var httpContextServiceMock = _mocker.GetMock<IHttpContextService>();
var checker = _mocker.CreateInstance<MethodConditionChecker>();

_httpContextServiceMock
httpContextServiceMock
.Setup(m => m.Method)
.Returns("GET");

// act
// Act
var result =
await _checker.ValidateAsync(new StubModel {Id = "id", Conditions = conditions}, CancellationToken.None);
await checker.ValidateAsync(new StubModel {Id = "id", Conditions = conditions}, CancellationToken.None);

// assert
// Assert
Assert.AreEqual(ConditionValidationType.Invalid, result.ConditionValidation);
}

[TestMethod]
public async Task MethodConditionChecker_ValidateAsync_StubsFound_HappyFlow()
{
// arrange
// Arrange
var conditions = new StubConditionsModel {Method = "GET"};
var httpContextServiceMock = _mocker.GetMock<IHttpContextService>();
var checker = _mocker.CreateInstance<MethodConditionChecker>();

_httpContextServiceMock
httpContextServiceMock
.Setup(m => m.Method)
.Returns("GET");

// act
// Act
var result =
await _checker.ValidateAsync(new StubModel {Id = "id", Conditions = conditions}, CancellationToken.None);
await checker.ValidateAsync(new StubModel {Id = "id", Conditions = conditions}, CancellationToken.None);

// assert
// Assert
Assert.AreEqual(ConditionValidationType.Valid, result.ConditionValidation);
}

[DataTestMethod]
[DataRow("GET", true)]
[DataRow("get", true)]
[DataRow("POST", true)]
[DataRow("post", true)]
[DataRow("PUT", false)]
[DataRow("put", false)]
public async Task MethodConditionChecker_ValidateAsync_MultipleMethods(string method, bool shouldPass)
{
// Arrange
var conditions = new StubConditionsModel {Method = new List<object> {"GET", "POST"}};
var httpContextServiceMock = _mocker.GetMock<IHttpContextService>();
var checker = _mocker.CreateInstance<MethodConditionChecker>();

httpContextServiceMock
.Setup(m => m.Method)
.Returns(method);

// Act
var result =
await checker.ValidateAsync(new StubModel {Id = "id", Conditions = conditions}, CancellationToken.None);

// Assert
Assert.AreEqual(shouldPass ? ConditionValidationType.Valid : ConditionValidationType.Invalid,
result.ConditionValidation);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using HttPlaceholder.Application.StubExecution.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace HttPlaceholder.Application.Tests.StubExecution.Utilities;
Expand Down Expand Up @@ -56,6 +58,41 @@ public void Convert_ConditionIsUnrecognized_ShouldThrowInvalidOperationException
Assert.ThrowsException<InvalidOperationException>(() =>
ConversionUtilities.Convert<StubConditionStringCheckingModel>(1234));

[TestMethod]
public void ConvertEnumerable_InputIsJArray_ShouldConvert()
{
// Arrange
var input = JArray.Parse(@"[""string1"", ""string2""]");

// Act
var result = ConversionUtilities.ConvertEnumerable<string>(input).ToArray();

// Assert
Assert.AreEqual(2, result.Length);
Assert.AreEqual("string1", result[0]);
Assert.AreEqual("string2", result[1]);
}

[TestMethod]
public void ConvertEnumerable_InputIsObjectList_ShouldConvert()
{
// Arrange
var input = new List<object> {"string1", "string2"};

// Act
var result = ConversionUtilities.ConvertEnumerable<string>(input).ToArray();

// Assert
Assert.AreEqual(2, result.Length);
Assert.AreEqual("string1", result[0]);
Assert.AreEqual("string2", result[1]);
}

[TestMethod]
public void ConvertEnumerable_InputIsUnrecognized_ShouldThrowInvalidOperationException() =>
// Act / Assert
Assert.ThrowsException<InvalidOperationException>(() => ConversionUtilities.Convert<string>(123));

[DataTestMethod]
[DataRow(3, 3)]
[DataRow((long)3, 3)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HttPlaceholder.Application.Infrastructure.DependencyInjection;
using HttPlaceholder.Application.Interfaces.Http;
using HttPlaceholder.Application.StubExecution.Utilities;
using HttPlaceholder.Domain;
using HttPlaceholder.Domain.Enums;

Expand All @@ -27,21 +29,25 @@ public MethodConditionChecker(IHttpContextService httpContextService)
public Task<ConditionCheckResultModel> ValidateAsync(StubModel stub, CancellationToken cancellationToken)
{
var result = new ConditionCheckResultModel();
var methodCondition = stub.Conditions?.Method;
if (string.IsNullOrEmpty(methodCondition))
var condition = stub.Conditions?.Method;
if (condition == null)
{
return Task.FromResult(result);
}

var method = _httpContextService.Method;
if (string.Equals(methodCondition, method, StringComparison.OrdinalIgnoreCase))
if ((condition is string methodCondition &&
string.Equals(methodCondition, method, StringComparison.OrdinalIgnoreCase)) || (condition is not string &&
ConversionUtilities
.ConvertEnumerable<string>(condition)
.Any(mc => string.Equals(mc, method, StringComparison.OrdinalIgnoreCase))))
{
// The path matches the provided regex. Add the stub ID to the resulting list.
// The path matches the provided condition. Add the stub ID to the resulting list.
result.ConditionValidation = ConditionValidationType.Valid;
}
else
{
result.Log = $"Condition '{methodCondition}' did not pass for request.";
result.Log = $"Condition '{condition}' did not pass for request.";
result.ConditionValidation = ConditionValidationType.Invalid;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using HttPlaceholder.Domain;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

Expand Down Expand Up @@ -33,7 +33,27 @@ public static T Convert<T>(object input)
return model;
default:
throw new InvalidOperationException(
$"Object of type '{input.GetType()}' not supported for serializing to '{typeof(StubConditionStringCheckingModel)}'.");
$"Object of type '{input.GetType()}' not supported for serializing to '{typeof(T)}'.");
}
}

/// <summary>
/// Converts the given input to an enumerable of type T.
/// </summary>
/// <param name="input">The input.</param>
/// <typeparam name="T">The type the input should be converted to.</typeparam>
/// <returns>The converted input.</returns>
public static IEnumerable<T> ConvertEnumerable<T>(object input)
{
switch (input)
{
case JArray jArray:
return jArray.ToObject<T[]>();
case IList<object> list:
return list.Select(i => (T)i);
default:
throw new InvalidOperationException(
$"Object of type '{input.GetType()}' not supported for serializing to '{typeof(T)}'.");
}
}

Expand Down
Loading

0 comments on commit 635a820

Please sign in to comment.