Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to generate json text field with random numeric content #884

Closed
robertmircea opened this issue Feb 12, 2023 · 24 comments
Closed

How to generate json text field with random numeric content #884

robertmircea opened this issue Feb 12, 2023 · 24 comments
Assignees
Labels

Comments

@robertmircea
Copy link

robertmircea commented Feb 12, 2023

I am trying to generate a json string field with random integers inside using response templating. Unfortunately, I cannot obtain the expected reply:

    var server = StandAloneApp.Start(settings);
    server.Given(Request.Create()
        .WithPath("/profile")
        .UsingGet()
    ).RespondWith(Response.Create()
        .WithTransformer()
        .WithBodyAsJson(new
        {
            puk = "{{Random Type=\"Integer\" Min=0 Max=2000000}}",
        })

produces a numeric field instead of a string:

{
      "puk": 1638351854,
}

instead of

{
      "puk": "1638351854",
}

How can I generate a random integer with min/max constraints formatted as a string?
Alternatively, I've tried "{{Random Type=\"TextRegex\" Pattern=\"[0-9]{10}\"}}" and the result is the same: numeric json field instead of string. It seems that the library is converting automatically any numeric string to numeric field value.

@StefH
Copy link
Collaborator

StefH commented Feb 12, 2023

Did you try adding \"?

puk = "\"{{Random Type=\"Integer\" Min=0 Max=2000000}}\"",

@robertmircea
Copy link
Author

It formats like this:

HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 12 Feb 2023 10:20:15 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
    "puk": "\"98039567\"",
}

@StefH
Copy link
Collaborator

StefH commented Mar 9, 2023

Hello @robertmircea , did you have time to verify this?

2 similar comments
@StefH
Copy link
Collaborator

StefH commented Mar 21, 2023

Hello @robertmircea , did you have time to verify this?

@StefH
Copy link
Collaborator

StefH commented May 12, 2023

Hello @robertmircea , did you have time to verify this?

@maivanquanbk
Copy link

Hi @StefH, when can I expect to have this feature? Thanks a lot

@StefH
Copy link
Collaborator

StefH commented Nov 1, 2023

@maivanquanbk
Did you have time to check that preview?

@felipetofoli
Copy link

As described in the issue #1028 , I am facing the same situation when parsing a DateTime "{{ DateTime.UtcNow \"yyMMddhhmmss\" }}" (after opening the issue I noticed the same applies for other dynamic fields, like outputs that resulted from lookup operation, etc.).

Just adding the comment here, so I can be notified with updates about this issue.

@StefH
Copy link
Collaborator

StefH commented Dec 9, 2023

#1036

@StefH
Copy link
Collaborator

StefH commented Dec 9, 2023

@robertmircea
@maivanquanbk
@felipetofoli

I've decided to change the logic as follows:

  1. By default, the value from Random or DateTime.Now or another Handlebars function will be converted to a primitive type. This is done to not break current functionality.

    So for example when a value can be converted to a string, it will be converted.

  2. If you do not want this, use this c# code : .WithTransformer(ReplaceNodeOptions.Evaluate)
    This will generate the puk code like:

    {
      "puk": "98039567"
    }

    And the timestamps like:

    {
      "timestamp": "231205110312",
      "timestamp2": "231205110312",
      "timestamp3": "231205110312"
    }

A preview version with this logic is : 1.5.42-ci-18041

@felipetofoli
Copy link

Hi @StefH , thanks for providing this new preview version!

I believe in topic 1. you meant: "So for example when a value can be converted to a primitive type, it will be converted.", right?

I tested the preview package and I received the timestamps as a string when applying the ReplaceNodeOptions.Evaluate option!


A question: What if we have a scenario where we need different behaviors at the "property" level?

Example: We need to return the current timestamp as a string, but a random number as an integer

{
    "timestamp": "20231211",
    "randomNumber": 358
}

How can we achieve the above result?
Maybe it would be interesting to have a way to convert(cast), format or force a certain type at the "property" level?
What do you think?

@StefH
Copy link
Collaborator

StefH commented Dec 12, 2023

@felipetofoli

"So for example when a value can be converted to a primitive type, it will be converted.", right?
Correct, if the string value is not any of the supported types, it will not be converted. So the string "abc" will not be converted to an integer.


I understand your request, I'm working on this, but it's difficult because the return value from Handlebars is always a string.

I'm working on a work-around :
Adding an Handlebars method named String.FormatAsString which you can use to convert the string to a wrapped / encoded string value which can be understood by WireMock so that in that case, no automatic conversion will be attempted.

A unit test for this could be:

    [Theory]
    [InlineData(ReplaceNodeOptions.EvaluateAndTryToConvert)]
    [InlineData(ReplaceNodeOptions.Evaluate)]
    public async Task Response_WithBodyAsJson_ProvideResponseAsync_Handlebars_DateTimeWithStringFormatAsString(ReplaceNodeOptions options)
    {
        // Assign
        var request = new RequestMessage(new UrlDetails("http://localhost"), "GET", ClientIp);

        var responseBuilder = Response.Create()
            .WithBodyAsJson(new
            {
                FormatAsString = "{{ String.FormatAsString (DateTime.UtcNow) \"yyMMddhhmmss\" }}"
            })
            .WithTransformer(options);

        // Act
        var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false);

        // Assert
        var jObject = JObject.FromObject(response.Message.BodyData!.BodyAsJson!);
        jObject["FormatAsString"]!.Type.Should().Be(JTokenType.String);
    }

However I need to think some more on this.

Maybe also a another method like Object.FormatAsValue should be added to always make sure that the string value is converted to a supported type....

@felipetofoli
Copy link

Thanks for your quick reply, @StefH !
Yes, this approach (at the property level) is flexible and it seems interesting!
Let me know if I can help you with anything!
Thanks!

@StefH
Copy link
Collaborator

StefH commented Dec 15, 2023

Thanks for your quick reply, @StefH ! Yes, this approach (at the property level) is flexible and it seems interesting! Let me know if I can help you with anything! Thanks!

You can test preview 1.5.43-ci-18089

@felipetofoli
Copy link

You can test preview 1.5.43-ci-18089

Hi @StefH !
I tested and the timestamps are returned as a string when using .WithTransformer(ReplaceNodeOptions.Evaluate).

Is there anything else I should test? Am I missing any new scenario that was implemented in this preview release?

@felipetofoli
Copy link

felipetofoli commented Dec 19, 2023

I checked the Handlebars repo, and I noticed that the String.FormatAsString method was implemented.
The String.FormatAsString is working! 😃

Test

Server config

server
    .Given(Request
            .Create()
            .WithPath(new ExactMatcher("/some-path"))
            .UsingPost())
    .RespondWith(Response
        .Create()
        .WithHeader("Content-Type", "application/json")
        .WithBodyAsJson(new
        {
            timestamp_number = "{{ DateTime.UtcNow \"yyMMddhhmmss\" }}",
            timestamp_string = "{{ String.FormatAsString (DateTime.UtcNow) \"yyMMddhhmmss\" }}"
        })
        .WithTransformer());

Response

{
    "timestamp_number": 231219053551,
    "timestamp_string": "231219053551"
}

Question

For a scenario where we don't have to specify a format (like a simple ToString()), is the expected passing an empty string?
Or we would like to have a new overload without a "format" param ({{ String.FormatAsString (request.PathSegments.[3]) }})?

.RespondWith(Response
    .Create()
    .WithHeader("Content-Type", "application/json")
    .WithBodyAsJson(new
    {
        somedata = new
        {
            frompath = "{{ String.FormatAsString (request.PathSegments.[3]) '' }}"
        }
    })
    .WithTransformer());

@StefH
Copy link
Collaborator

StefH commented Dec 19, 2023

I think you can just omit the format. No need for passing an empty string.

Can you try that?

@felipetofoli
Copy link

felipetofoli commented Dec 19, 2023

I think you can just omit the format. No need for passing an empty string.

Can you try that?

I'd tried that. It's not possible.

Test

Server config

somedata = new
{
    frompath = "{{ String.FormatAsString (request.PathSegments.[3]) }}"
}

Response

HTTP/1.1 500 Internal Server Error 
{"Status":"The String.FormatAsString helper must have exactly 2 arguments."}

@StefH
Copy link
Collaborator

StefH commented Dec 20, 2023

@felipetofoli
I see, this is not yet supported.

I've update Handlebars.Net.Helpers (Handlebars-Net/Handlebars.Net.Helpers#87) and I'll release a new NuGet and build a new preview for WireMock.Net

Stay tuned....

@felipetofoli
Copy link

@StefH
Thanks for all your support! 😃

@StefH
Copy link
Collaborator

StefH commented Dec 20, 2023

Can you try preview 1.5.43-ci-18158 ?

@felipetofoli
Copy link

felipetofoli commented Dec 21, 2023

Can you try preview 1.5.43-ci-18158 ?

@StefH I tried the preview package:

Tests

EvaluateAndTryToConvert (or default: .WithTransformer())

server
    .Given(Request
        .Create()
        .WithPath(new ExactMatcher("/type-conversion"))
        .UsingPost())
    .RespondWith(Response
        .Create()
        .WithHeader("Content-Type", "application/json")
        .WithBodyAsJson(new
        {
            timestamp = "{{ DateTime.UtcNow \"yyMMddhhmmss\" }}",
            timestamp_as_string = "{{ String.FormatAsString (DateTime.UtcNow) \"yyMMddhhmmss\" }}",
            timestamp_as_string_without_format = "{{ String.FormatAsString (DateTime.UtcNow) }}",

            random_number = "{{ Random Type='Integer' Min=1 Max=100 }}",
            random_number_as_number = "{{ Object.FormatAsObject (Random Type='Integer' Min=1 Max=100) }}",
            random_number_as_string = "{{ String.FormatAsString (Random Type='Integer' Min=1 Max=100) }}",

            number_from_body = "{{ request.bodyAsJson.number }}",
            number_from_body_as_number = "{{ Object.FormatAsObject (request.bodyAsJson.number) }}",
            number_from_body_as_string = "{{ String.FormatAsString (request.bodyAsJson.number) }}",

            numeric_string_from_body = "{{ request.bodyAsJson.numeric_string }}",
            numeric_string_from_body_as_number = "{{ Object.FormatAsObject (request.bodyAsJson.numeric_string) }}",
            numeric_string_from_body_as_string = "{{ String.FormatAsString (request.bodyAsJson.numeric_string) }}",
        })
        .WithTransformer(WireMock.Types.ReplaceNodeOptions.EvaluateAndTryToConvert));

HTTP Request/Response:

POST /type-conversion HTTP/1.1
Content-Type: application/json

{
  "number" : 999,
  "numeric_string": "011"
}
 
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 21 Dec 2023 18:10:08 GMT
 
{
 
 "timestamp":231221061008, 
 "timestamp_as_string":"231221061008",
 "timestamp_as_string_without_format":"21/12/2023 18:10:08",
 "random_number":43,
 "random_number_as_number":8,
 "random_number_as_string":"2",
 "number_from_body":999,
 "number_from_body_as_number":999,
 "number_from_body_as_string":"999",
 "numeric_string_from_body":"011",
 "numeric_string_from_body_as_number":11,
 "numeric_string_from_body_as_string":"011"
}

Evaluate

server
    .Given(Request
        .Create()
        .WithPath(new ExactMatcher("/type-conversion"))
        .UsingPost())
    .RespondWith(Response
        .Create()
        .WithHeader("Content-Type", "application/json")
        .WithBodyAsJson(new
        {
            timestamp = "{{ DateTime.UtcNow \"yyMMddhhmmss\" }}",
            timestamp_as_string = "{{ String.FormatAsString (DateTime.UtcNow) \"yyMMddhhmmss\" }}",
            timestamp_as_string_without_format = "{{ String.FormatAsString (DateTime.UtcNow) }}",

            random_number = "{{ Random Type='Integer' Min=1 Max=100 }}",
            random_number_as_number = "{{ Object.FormatAsObject (Random Type='Integer' Min=1 Max=100) }}",
            random_number_as_string = "{{ String.FormatAsString (Random Type='Integer' Min=1 Max=100) }}",

            number_from_body = "{{ request.bodyAsJson.number }}",
            number_from_body_as_number = "{{ Object.FormatAsObject (request.bodyAsJson.number) }}",
            number_from_body_as_string = "{{ String.FormatAsString (request.bodyAsJson.number) }}",

            numeric_string_from_body = "{{ request.bodyAsJson.numeric_string }}",
            numeric_string_from_body_as_number = "{{ Object.FormatAsObject (request.bodyAsJson.numeric_string) }}",
            numeric_string_from_body_as_string = "{{ String.FormatAsString (request.bodyAsJson.numeric_string) }}",
        })
        .WithTransformer(WireMock.Types.ReplaceNodeOptions.Evaluate));

HTTP Request/Response:

POST /type-conversion HTTP/1.1
Content-Type: application/json
 
{
  "number" : 999,
  "numeric_string": "011"
}
 
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 21 Dec 2023 18:14:42 GMT
 
{ 
  "timestamp":"231221061442",
  "timestamp_as_string":"231221061442",
  "timestamp_as_string_without_format":"21/12/2023 18:14:42",
  "random_number":"51",
  "random_number_as_number":"46",
  "random_number_as_string":"45",
  "number_from_body":999,
  "number_from_body_as_number":"999",
  "number_from_body_as_string":"999",
  "numeric_string_from_body":"011",
  "numeric_string_from_body_as_number":"011",
  "numeric_string_from_body_as_string":"011"
}

Results

  • String.FormatAsString looks good!

  • ⚠️ Object.FormatAsObject doesn't seem to work when used with ReplaceNodeOptions.Evaluate (check random_number_as_number, number_from_body_as_number, numeric_string_from_body_as_number).

I am not sure if I am missing something here... But I think the following unit test should be satisfied, right? Or do you want to deal with FormatAsNumber in another moment?

[Theory]
[InlineData(ReplaceNodeOptions.EvaluateAndTryToConvert)]
[InlineData(ReplaceNodeOptions.Evaluate)]
public async Task Response_WithBodyAsJson_ProvideResponseAsync_Handlebars_RandomIntegerWithObjectFormatAsNumber(ReplaceNodeOptions options)
{
    // Assign
    var request = new RequestMessage(new UrlDetails("http://localhost"), "GET", ClientIp);

    var responseBuilder = Response.Create()
        .WithBodyAsJson(new
        {
            FormatAsNumber = "{{ Object.FormatAsObject (Random Type='Integer' Min=1 Max=100) }}"
        })
        .WithTransformer(options);

    // Act
    var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false);

    // Assert
    var jObject = JObject.FromObject(response.Message.BodyData!.BodyAsJson!);
    jObject["FormatAsNumber"]!.Type.Should().Be(JTokenType.Integer);    
}

@StefH
Copy link
Collaborator

StefH commented Dec 21, 2023

FormatAsObject does indeed not work as expected.
However I need to think on that, probably will be fixed in future.

I'll merge the PR and create a new official NuGet.

@StefH StefH closed this as completed Dec 21, 2023
@felipetofoli
Copy link

Thanks, @StefH !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants