Skip to content

Routes are determined on first match, not the most complete match #144

@lanegoolsby

Description

@lanegoolsby

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure a similar issue has not already been created

Description

If you have multiple mocks setup that are similar, but one is fully formed while the others have wildcards (i.e. {}'s), Mockaco returns the first match it finds, not the mock with the best match.

The order in which Mockaco searches appears to be file-name driven. Meaning it will match a.json before z.json, even if z.json has a fully formed route whereas a.json has wildcards.

For example:

a.json
route: /api/{foo}/{bar}
b.json
route: /api/path/{bar}
c.json
route: /api/{foo}/file
z.json
route: /api/path/file

When calling /api/path/file Mockaco currently matches a.json, b.json, or c.json first because their file names are first alphabetically even though z.json is a complete match.

Proposed solution

Mockaco should look at all defined route matches and choose the route with the most complete match, not the first match it finds.

I propose changing this logic to look something like this:

//snip
            Mock bestMatch = null;
            var bestScore = - 1;
            
            foreach (var mock in mockProvider.GetMocks())
            {
                if (await requestMatchers.AllAsync(e => e.IsMatch(httpContext.Request, mock)))
                {
                    var score = ScoreRouteTemplate(mock.Route);

                    if (score > bestScore)
                    {
                        bestMatch = mock;
                        bestScore = score;
                    }
                }
                else
                {
                    _logger.LogDebug("Incoming request didn't match {mock}", mock);
                }
            }

            if (bestMatch != null)
            {
                cache.Set($"{nameof(RequestMatchingMiddleware)} {httpContext.Request.Path.Value}",new
                {
                    Route = httpContext.Request.Path.Value,
                    Timestamp = $"{DateTime.Now:t}",
                    Headers = LoadHeaders(httpContext, options.Value.VerificationIgnoredHeaders),
                    Body = await httpContext.Request.ReadBodyStream()
                }, DateTime.Now.AddMinutes(options.Value.MatchedRoutesCacheDuration));

                _logger.LogInformation("Incoming request matched {mock}", bestMatch);

                await scriptContext.AttachRouteParameters(httpContext.Request, bestMatch);

                var template = await templateTransformer.TransformAndSetVariables(bestMatch.RawTemplate, scriptContext);

                mockacoContext.Mock = bestMatch;
                mockacoContext.TransformedTemplate = template;

                await _next(httpContext);

                return;
            }
//snip

    private int ScoreRouteTemplate(string routeTemplate)
    {
        var segments = routeTemplate.Split('/');
        int score = 0;

        foreach (var segment in segments)
        {
            if (segment.StartsWith("{") && segment.EndsWith("}"))
            {
                // This is a wildcard or route parameter, lower score
                score += 1;
            }
            else
            {
                // Explicit path segment, higher score
                score += 10;
            }
        }

        return score;
    }

Note that ScoreRouteTemplate doesn't really handle b.json and c.json in the problem description. It would need a little more work to rank the higher cardinality paths (i.e. /api/foo/{bar}) higher than lower cardinatity (i.e. /api/{foo}/bar). Before I spend more time on it I wanted to get feedback on the approach.

Alternatives

I've tried using file names in creative ways but its becoming difficult to manage.

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions