diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 42ad4b093..1db5302b1 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -68,15 +68,15 @@ jobs: build-cake: needs: build runs-on: ubuntu-latest - environment: build.cake + # environment: build.cake env: # https://github.com/actions/setup-dotnet/blob/main/README.md#environment-variables # NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages DOTNET_INSTALL_DIR: "/usr/share/dotnet" # don't override by /usr/lib/dotnet - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - GITHUB_SHA: ${{ github.sha }} - GITHUB_REF: ${{ github.ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} + # COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + # GITHUB_SHA: ${{ github.sha }} + # GITHUB_REF: ${{ github.ref }} + # GITHUB_REF_NAME: ${{ github.ref_name }} steps: - name: .NET Info run: | @@ -144,9 +144,12 @@ jobs: target: Build - name: Prepare Coveralls run: | - echo "GITHUB_REF is ${{ env.GITHUB_REF }}" - echo "GITHUB_REF_NAME is ${{ env.GITHUB_REF_NAME }}" - echo "GITHUB_SHA is ${{ env.GITHUB_SHA }}" + # echo "GITHUB_REF is ${{ env.GITHUB_REF }}" + # echo "GITHUB_REF_NAME is ${{ env.GITHUB_REF_NAME }}" + # echo "GITHUB_SHA is ${{ env.GITHUB_SHA }}" + echo "Listing environment variables:" + env | sort + echo ------------ Detect coverage file ------------ coverage_1st_folder=$(ls -d /home/runner/work/Ocelot/Ocelot/artifacts/UnitTests/*/ | head -1) echo "Detected first folder : $coverage_1st_folder" coverage_file="${coverage_1st_folder%/}/coverage.cobertura.xml" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6e20addee..55a6723f7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -82,12 +82,12 @@ jobs: build-cake: needs: build runs-on: ubuntu-latest - environment: build.cake - env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - GITHUB_SHA: ${{ github.sha }} - GITHUB_REF: ${{ github.ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} + # environment: Pull-Request + # env: + # COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + # GITHUB_SHA: ${{ github.sha }} + # GITHUB_REF: ${{ github.ref }} + # GITHUB_REF_NAME: ${{ github.ref_name }} steps: - name: .NET Info run: | diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 1ddd12432..bb0cf757f 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -119,10 +119,13 @@ Here is the complete dynamic route configuration, also known as the *"dynamic ro { "DownstreamHttpVersion": "", "DownstreamHttpVersionPolicy": "", + "LoadBalancerOptions": {}, "Metadata": {}, // dictionary + "QoSOptions": {}, "RateLimitRule": {}, // deprecated! -> use RateLimitOptions "RateLimitOptions": {}, "ServiceName": "", + "ServiceNamespace": "", "Timeout": 0 // nullable integer } @@ -181,6 +184,7 @@ Here is the complete global configuration, also known as the *"global configurat "DownstreamScheme": "", "HttpHandlerOptions": {}, "LoadBalancerOptions": {}, + "Metadata": {}, // dictionary "MetadataOptions": {}, "QoSOptions": {}, "RateLimitOptions": {}, @@ -731,7 +735,7 @@ The ``Metadata`` options can store any arbitrary data that users can access in m By using the *metadata*, users can implement their own logic and extend the functionality of Ocelot. The :doc:`../features/metadata` feature is designed to extend both the static :ref:`config-route-schema` and :ref:`config-dynamic-route-schema`. -Global *metadata* must be defined inside the ``MetadataOptions`` section. +Global *metadata* must be defined in the ``Metadata`` section, while parsing options should be placed in the ``MetadataOptions`` section. The following example demonstrates practical usage of this feature: @@ -754,12 +758,12 @@ The following example demonstrates practical usage of this feature: ], "GlobalConfiguration": { // other opts... + "Metadata": { + "instance_name": "dc-1-54abcz", + "my-extension/param1": "default-value" + }, "MetadataOptions": { - // other metadata opts... - "Metadata": { - "instance_name": "dc-1-54abcz", - "my-extension/param1": "default-value" - } + // parsing metadata opts... } } } diff --git a/docs/features/loadbalancer.rst b/docs/features/loadbalancer.rst index 567f98c69..bc798cad9 100644 --- a/docs/features/loadbalancer.rst +++ b/docs/features/loadbalancer.rst @@ -14,17 +14,31 @@ This means you can scale your downstream services, and Ocelot can use them effec Class: `FileLoadBalancerOptions`_ -Here is the complete *load balancer* configuration (the schema) of top-level properties. -You do not need to set all of these options, but the ``Type`` option is required. +The following is the full *load balancer* configuration, used in both the :ref:`config-route-schema` and the :ref:`config-dynamic-route-schema`. +Not all of these options need to be configured; however, the ``Type`` option is mandatory. .. code-block:: json "LoadBalancerOptions": { - "Expiry": 2147483647, - "Key": "", - "Type": "" + "Type": "", + "Key": "", // CookieStickySessions balancer + "Expiry": 1 // ms, CookieStickySessions balancer } +.. list-table:: + :widths: 15 85 + :header-rows: 1 + + * - *Option* + - *Description* + * - ``Type`` + - An in-built *load balancer* type selected from the list of available :ref:`lb-balancers`, or a user-defined type (refer to the ":ref:`Custom Balancers `" section). + * - ``Key`` + - The name of the cookie you wish to use for sticky sessions. This option is applicable only to the :ref:`CookieStickySessions type `. + * - ``Expiry`` + - Expiration period specifies how long, in milliseconds, the session should remain sticky. + This value refreshes with each request to mimic typical session behavior. Note: This option applies only to the :ref:`CookieStickySessions type `. + The actual ``LoadBalancerOptions`` schema with all the properties can be found in the C# `FileLoadBalancerOptions`_ class. .. _lb-configuration: @@ -32,27 +46,8 @@ The actual ``LoadBalancerOptions`` schema with all the properties can be found i Configuration ------------- -The types of *load balancer* available are: - -.. list-table:: - :widths: 25 75 - :header-rows: 1 - - * - *Type* - - *Description* - * - ``CookieStickySessions`` - - This uses a cookie to stick all requests to a specific server. More information can be found in the :ref:`lb-cookiestickysessions-type` section. - * - ``LeastConnection`` - - This tracks which services are dealing with requests and sends new requests to the service with the fewest ("least") existing requests. The algorithm state is not distributed across a cluster of Ocelots. - * - ``NoLoadBalancer`` - - This takes the first available service from configuration or service discovery. - * - ``RoundRobin`` - - This loops through available services and sends requests. The algorithm state is not distributed across a cluster of Ocelots. - -You must choose which *load balancer* to use in your configuration. - -The following shows how to set up multiple downstream services for a route using `ocelot.json`_ and then select the ``LeastConnection`` *load balancer*. -This is the simplest way to set up load balancing. +The following shows how to set up multiple downstream services for a static route using `ocelot.json`_ and then select the ``LeastConnection`` *load balancer*. +This is the simplest way to configure load balancing without using service discovery. .. code-block:: json @@ -70,10 +65,7 @@ This is the simplest way to set up load balancing. } } -Service Discovery [#f1]_ ------------------------- - -The following shows how to set up a route using :doc:`../features/servicediscovery` and then select the ``LeastConnection`` *load balancer*. +The following shows how to set up a route using :doc:`../features/servicediscovery` and then select the ``RoundRobin`` *load balancer*. .. code-block:: json @@ -81,7 +73,7 @@ The following shows how to set up a route using :doc:`../features/servicediscove // ... "ServiceName": "product", "LoadBalancerOptions": { - "Type": "LeastConnection" + "Type": "RoundRobin" } } @@ -89,9 +81,104 @@ When this is set up, Ocelot will look up the downstream host and port from the : If you add and remove services from the :doc:`../features/servicediscovery` provider [#f1]_, Ocelot should respect this and stop calling services that have been removed and start calling services that have been added. +.. _lb-global-configuration: + +Global Configuration [#f2]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A complete configuration consists of both route-level and global *load balancing*. +You can configure the following options in the ``GlobalConfiguration`` section of `ocelot.json`_: + +.. code-block:: json + + "Routes": [ + { + "Key": "R0", // optional + "LoadBalancerOptions": { + "Type": "CookieStickySessions", + "Key": ".AspNetCore.Session", + "Expiry": 1200000 // milliseconds, 20 minutes + } + }, + { + "Key": "R1", // this route is part of a group + "LoadBalancerOptions": {} // optional because of grouping + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://ocelot.net", + "LoadBalancerOptions": { + "RouteKeys": ["R1"], // if undefined or empty array, opts will apply to all routes + "Type": "LeastConnection" + } + } + +:doc:`../features/servicediscovery` dynamic routes intentionally override the global :ref:`dynamic routing ` configuration: + +.. code-block:: json + + "DynamicRoutes": [ + { + "Key": "", // optional + "ServiceName": "my-service", + "LoadBalancerOptions": { + "Type": "LeastConnection" // switch from RoundRobin to LeastConnection + } + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://ocelot.net", + "DownstreamScheme": "http", + "ServiceDiscoveryProvider": { + // required section for dynamic routing + }, + "LoadBalancerOptions": { + "RouteKeys": [], // no grouping, thus opts apply to all dynamic routes + "Type": "RoundRobin" + } + } + +In this configuration, the ``RoundRobin`` balancer is used for all implicit dynamic routes. +However, for the "my-service" service, the load balancer type has been explicitly switched from ``RoundRobin`` to ``LeastConnection``. + +.. note:: + + 1. If the ``RouteKeys`` option is not defined or the array is empty in the global ``LoadBalancerOptions``, the global options will apply to all routes. + If the array contains route keys, it defines a single group of routes to which the global options apply. + Routes excluded from this group must specify their own route-level ``LoadBalancerOptions``. + + 2. Prior to version `24.1`_, global ``LoadBalancerOptions`` were only accessible in the special :ref:`Dynamic Routing ` mode. + Since version `24.1`_, global configuration has been available for both static and dynamic routes. + As a team, we would consider the idea of implementing such a global configuration for aggregated routes. + However, an aggregated route is essentially a combination of static routes. + +.. _lb-balancers: + +Balancers +--------- + +The available types of built-in *load balancers* are: + +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - *Type* + - *Description* + * - ``CookieStickySessions`` + - This uses a cookie to stick all requests to a specific server. More information can be found in the ":ref:`CookieStickySessions Type`" section. + * - ``LeastConnection`` + - This tracks which services are dealing with requests and sends new requests to the service with the fewest ("least") existing requests. The algorithm state is not distributed across a cluster of Ocelots. + * - ``RoundRobin`` + - This loops through available services and sends requests. The algorithm state is not distributed across a cluster of Ocelots. + * - ``NoLoadBalancer`` + - This takes the first available service from :ref:`configuration ` or :doc:`../features/servicediscovery` provider. + +You must choose which *load balancer* to use in your :ref:`configuration `. + .. _lb-cookiestickysessions-type: -``CookieStickySessions`` Type [#f2]_ +``CookieStickySessions`` Type [#f3]_ ------------------------------------ We have implemented a basic sticky session type of *load balancer*. @@ -113,27 +200,13 @@ In order to set up the ``CookieStickySessions`` *load balancer*, you need to do ], "LoadBalancerOptions": { "Type": "CookieStickySessions", - "Key": "ASP.NET_SessionId", - "Expiry": 1800000 // milliseconds + "Key": ".AspNetCore.Session", + "Expiry": 1200000 // milliseconds, 20 minutes } } -The ``LoadBalancerOptions`` are: - -.. list-table:: - :widths: 15 85 - :header-rows: 1 - - * - *Option* - - *Description* - * - ``Type`` - - This needs to be ``CookieStickySessions``. - * - ``Key`` - - This is the key of the cookie you wish to use for the sticky sessions. - * - ``Expiry`` - - This is how long, in milliseconds, you want the session to be stuck for. Remember, this refreshes on every request, which is meant to mimic how sessions usually work. - -.. _break: http://break.do +These ``LoadBalancerOptions`` configure the ``CookieStickySessions`` load balancer using the standard session cookie ``Key`` for ASP.NET Core apps with sessions enabled. +The default expiration time is 20 minutes, matching the default session timeout in ASP.NET Core. **Note 1**: If you have multiple routes with the same ``LoadBalancerOptions``, then all of those routes will use the same *load balancer* for their subsequent requests. This means the sessions will be stuck across routes. @@ -143,7 +216,7 @@ The ``LoadBalancerOptions`` are: .. _lb-custom-balancers: -Custom Balancers [#f3]_ +Custom Balancers [#f4]_ ----------------------- In order to create and use a custom *load balancer*, you can do the following. @@ -196,8 +269,7 @@ Then, you need to create a class that implements the ``ILoadBalancer`` interface } Finally, you need to register this class with Ocelot. - -1. We have used the most complex example below to show all of the data and types that can be passed into the factory that creates *load balancers*. +We have used the most complex example below to show all of the data and types that can be passed into the factory that creates *load balancers*. .. code-block:: csharp @@ -211,7 +283,7 @@ Finally, you need to register this class with Ocelot. .AddOcelot(builder.Configuration) .AddCustomLoadBalancer(lbFactory); -2. However, there is a much simpler example that will work the same way: +However, there is a much simpler example that will work the same way: .. code-block:: csharp @@ -221,13 +293,12 @@ Finally, you need to register this class with Ocelot. .AddOcelot(builder.Configuration) .AddCustomLoadBalancer(); -Notes ------ +.. note:: -1. There are numerous ``IOcelotBuilder`` `methods `_ to add a custom *load balancer*. - The interface is as follows: + 1. There are numerous ``IOcelotBuilder`` `methods `_ to add a custom *load balancer*. + The interface is as follows: - .. code-block:: csharp + .. code-block:: csharp IOcelotBuilder AddCustomLoadBalancer() where T : ILoadBalancer, new(); @@ -240,24 +311,30 @@ Notes IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where T : ILoadBalancer; -2. When you enable custom *load balancers*, Ocelot looks up your *load balancer* by its class name when it decides whether to perform load balancing. + 2. When you enable custom *load balancers*, Ocelot looks up your *load balancer* by its class name when it decides whether to perform load balancing. + + * If it finds a match, it will use your load balancer to load balance. + * If Ocelot cannot match the *load balancer* type in your configuration with the name of the registered *load balancer* class, then you will receive an HTTP `500 Internal Server Error`_. + * If your *load balancer* factory throws an exception when Ocelot calls it, you will receive an HTTP `500 Internal Server Error`_. - * If it finds a match, it will use your load balancer to load balance. - * If Ocelot cannot match the *load balancer* type in your configuration with the name of the registered *load balancer* class, then you will receive an HTTP `500 Internal Server Error`_. - * If your *load balancer* factory throws an exception when Ocelot calls it, you will receive an HTTP `500 Internal Server Error`_. +.. warning:: -3. Remember, if you specify no *load balancer* in your :ref:`lb-configuration`, Ocelot will not attempt to load balance. + Remember, if you specify no *load balancer* in your :ref:`lb-configuration`, Ocelot will not attempt to load balance. """" -.. [#f1] Currently supported :doc:`../features/servicediscovery` providers are :ref:`sd-consul`, :doc:`../features/kubernetes`, :ref:`sd-eureka`, :doc:`../features/servicefabric`, and manually developed :ref:`sd-custom-providers`. -.. [#f2] The ":ref:`lb-cookiestickysessions-type`" feature was requested in issue `322`_, though what the user wants is more complicated than just sticky sessions. Anyway, we thought this would be a nice feature to have! Initially, the feature was released in version `6.0.0`_. -.. [#f3] The ":ref:`lb-custom-balancers`" feature by `David Lievrouw`_ implemented a way to provide Ocelot with a custom *load balancer* in PR `1155`_ (his issue `961`_, released in version `15.0.3`_). +.. [#f1] Currently supported :doc:`../features/servicediscovery` providers are :ref:`sd-consul`, :doc:`Kubernetes <../features/kubernetes>`, :ref:`Eureka `, :doc:`../features/servicefabric`, and manually developed :ref:`sd-custom-providers`. +.. [#f2] The ":ref:`Global Configuration `" feature, as part of issue `585`_, was introduced in pull request `2324`_ and released in version `24.1`_. +.. [#f3] The ":ref:`CookieStickySessions Type `" feature was requested in issue `322`_, though what the user wants is more complicated than just sticky sessions. Anyway, we thought this would be a nice feature to have! Initially, the feature was released in version `6.0.0`_. +.. [#f4] The ":ref:`Custom Balancers `" feature by `David Lievrouw`_ implemented a way to provide Ocelot with a custom *load balancer* in pull request `1155`_ (issue `961`_, released in version `15.0.3`_). .. _322: https://github.com/ThreeMammals/Ocelot/issues/322 +.. _585: https://github.com/ThreeMammals/Ocelot/issues/585 .. _961: https://github.com/ThreeMammals/Ocelot/issues/961 .. _1155: https://github.com/ThreeMammals/Ocelot/pull/1155 +.. _2324: https://github.com/ThreeMammals/Ocelot/pull/2324 .. _6.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/6.0.0 .. _15.0.3: https://github.com/ThreeMammals/Ocelot/releases/tag/15.0.3 +.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _David Lievrouw: https://github.com/DavidLievrouw .. _500 Internal Server Error: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 diff --git a/docs/features/metadata.rst b/docs/features/metadata.rst index 255aa5da6..d40152814 100644 --- a/docs/features/metadata.rst +++ b/docs/features/metadata.rst @@ -21,20 +21,22 @@ As you may already know from the :doc:`../features/configuration` chapter and th .. _FileMetadataOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileMetadataOptions.cs - Class: `FileMetadataOptions`_ - -But there is **global** *metadata* configuration: the ``MetadataOptions`` *schema*. +However, **global** metadata configuration consists of both the ``Metadata`` and ``MetadataOptions`` sections. You do not need to set all of these things, but this is everything that is available at the moment. .. code-block:: json - "MetadataOptions": { - "CurrentCulture": "en-GB", - "NumberStyle": "Any", - "Separators": [","], - "StringSplitOption": "None", - "TrimChars": [" "], - "Metadata": {} // dictionary + "GlobalConfiguration": { + "Metadata": { + // "key": "value", + }, + "MetadataOptions": { + "CurrentCulture": "en-GB", + "NumberStyle": "Any", + "Separators": [","], + "StringSplitOption": "None", + "TrimChars": [" "], + } } The actual global *metadata* schema with all the properties can be found in the C# `FileMetadataOptions`_ class. @@ -91,11 +93,11 @@ By using the *metadata*, users can implement their own logic and extend the func } ], "GlobalConfiguration": { + "Metadata": { + "instance_name": "machine-1", + "plugin2/param1": "default-value" + }, "MetadataOptions": { - "Metadata": { - "instance_name": "machine-1", - "plugin2/param1": "default-value" - } } } } diff --git a/docs/features/qualityofservice.rst b/docs/features/qualityofservice.rst index 074d910e8..e183fe27d 100644 --- a/docs/features/qualityofservice.rst +++ b/docs/features/qualityofservice.rst @@ -106,6 +106,10 @@ According to the :ref:`config-global-configuration-schema`, it is possible to co Please note that route-level options take precedence over global options. + **Note**: Dynamic routes were not supported in versions prior to `24.1`_. + Beginning with version `24.1`_, global *QoS* options for :ref:`Dynamic Routing ` may be overridden in the ``DynamicRoutes`` configuration section, as defined by the :ref:`config-dynamic-route-schema`. + Additionally, global configuration for static routes (also known as ``Routes``) has been supported since version `24.1`_. + .. _qos-circuit-breaker-strategy: Circuit Breaker strategy @@ -169,7 +173,7 @@ The ``TimeoutValue`` can be configured independently from the options of the :re This setup activates only the `Timeout resilience strategy`_. -To configure a global QoS timeout using the *Timeout strategy* for all static routes (excluding :ref:`qos-notes-unsupported-dynamic-routes`), set the ``TimeoutValue`` option according to the :ref:`config-global-configuration-schema`: +To configure a global QoS timeout using the *Timeout strategy* for all routes (both static and dynamic) set the ``TimeoutValue`` option as defined in the :ref:`config-global-configuration-schema`: .. code-block:: json @@ -254,16 +258,6 @@ Global and default QoS timeouts If a route-level *QoS* timeout is undefined, the global ``TimeoutValue`` takes precedence over the default timeout (30 seconds, see the `Timeout`_ docs). This means the global *QoS* timeout can override Polly's default of `30 seconds `_ via the :ref:`config-global-configuration-schema`. -.. _qos-notes-unsupported-dynamic-routes: - -Unsupported dynamic routes -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Both route-level and global *QoS* options apply only to static routes, as defined by the :ref:`config-route-schema`. -Since the :ref:`config-dynamic-route-schema` does not support *QoS* options, *Quality of Service* is not applied to dynamic routes in :ref:`Dynamic Routing ` mode. -However, global configuration remains possible in :ref:`Dynamic Routing ` mode. -Therefore, it is not possible to override global *QoS* options using dynamic route-level *QoS* settings. - .. _qos-extensibility: Extensibility [#f4]_ diff --git a/docs/features/ratelimiting.rst b/docs/features/ratelimiting.rst index 510f38004..908168393 100644 --- a/docs/features/ratelimiting.rst +++ b/docs/features/ratelimiting.rst @@ -51,10 +51,7 @@ Additionally, the :ref:`config-global-configuration-schema` allows configuring g If the ``RouteKeys`` option is not defined in the global ``RateLimitOptions``, the global settings will apply to all routes. **Note 2**: You do not need to set all of these options due to default values, but the following rule options are required: ``Limit`` and ``Period``. - If these required options are undefined and no global configuration is present, the route will be blocked by default: using a zero-limit over the default period ('1s', one second). - An undefined or misconfigured zero limit results in a `503 Service Unavailable`_ status code, along with a logged warning and the following message in the response body: - *"Rate limiting is misconfigured for the route '/{route_name}' due to an invalid rule -> {rule} !"* - Required and optional options are explained in the :ref:`rl-configuration` table below. + If these required options are undefined and no global configuration is present, Ocelot will fail to start due to an internally generated validation error, which will be visible in the logs. **Note 3**: Several :ref:`deprecated options ` originating from version `24.0`_ and earlier (see `old schema`_) are retained for one release cycle. Both introduced and :ref:`deprecated options ` are detailed in the :ref:`rl-configuration` table below. @@ -146,7 +143,7 @@ You can configure the following options in the ``GlobalConfiguration`` section o **Note 2**: The string values for the ``Period`` and ``Wait`` options must contain a floating-point number followed by one of the supported time units: 'ms', 's', 'm', 'h', or 'd'. If no unit is specified, the value defaults to milliseconds. For example, "333.5" is interpreted as 333 milliseconds and 500 microseconds (equivalent to "333.5ms"). The floating-point component may be omitted; for example, "10.0s" is equivalent to "10s". - These values are parsed dynamically at runtime, so there is no early fluent validation of the options in `ocelot.json`_ when the Ocelot app starts. + These values are parsed dynamically at runtime, so the required ``Period`` option in `ocelot.json`_ is validated early through fluent validation when the Ocelot app starts. If an invalid value is provided, the *Rate Limiting* middleware will throw a ``FormatException``, which is logged accordingly. .. _rl-deprecated-options: diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index 3f2e89c73..e13c15cd6 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -389,11 +389,11 @@ Ocelot will append any query string to the downstream URL as usual. Currently, dynamic routes and static routes cannot be mixed. Additionally, you need to specify the details of the *service discovery* provider as outlined above, along with the downstream ``http(s)`` scheme under ``DownstreamScheme``. - In addition to the global ``ServiceDiscoveryProvider`` section, the :ref:`config-global-configuration-schema` includes configurable options such as ``RateLimitOptions``, ``QoSOptions``, ``LoadBalancerOptions``, ``HttpHandlerOptions``, and ``DownstreamScheme``. + In addition to the global ``ServiceDiscoveryProvider`` section, the :ref:`config-global-configuration-schema` includes configurable options such as ``CacheOptions``, ``RateLimitOptions``, ``QoSOptions``, ``LoadBalancerOptions``, ``HttpHandlerOptions``, and ``DownstreamScheme``. These options are applicable to all dynamic routes, globally. - However, since the :ref:`config-dynamic-route-schema` does not support these options (except for ``RateLimitOptions``), they are not applied in *dynamic routing* mode. + However, since the :ref:`config-dynamic-route-schema` does not support these options (except for ``LoadBalancerOptions`` and ``RateLimitOptions``), they are not applied in *dynamic routing* mode. Therefore, it is not possible to override global options using dynamic route-level settings. - To reiterate, the only options fully supported by both static and dynamic routes are ``RateLimitOptions``. + To reiterate, the only options fully supported by both static and dynamic routes are ``LoadBalancerOptions`` and ``RateLimitOptions``. For instance, when exposing Ocelot publicly over HTTPS while routing to internal services over HTTP, your configuration may resemble the following: @@ -410,7 +410,8 @@ For instance, when exposing Ocelot publicly over HTTPS while routing to internal "ServiceDiscoveryProvider": { "Host": "localhost", // if Consul is hosted on the same machine as Ocelot "Port": 8500, - "Type": "Consul" + "Type": "Consul", + "Namespace": "" // not supported for Consul, but supported for Kubernetes }, "RateLimitOptions": { "ClientIdHeader": "Oc-DynamicRouting-Client", @@ -433,6 +434,11 @@ For instance, when exposing Ocelot publicly over HTTPS while routing to internal } } +.. _sd-dynamic-routing-configuration: + +Configuration +^^^^^^^^^^^^^ + Ocelot also allows configuration of a ``DynamicRoutes`` collection consisting of :ref:`config-dynamic-route-schema` objects. This enables overriding ``RateLimitOptions`` for each downstream service, along with other schema-level overrides. Dynamic route options are particularly useful when there are multiple services—such as a 'product' service and a 'search' service—and stricter rate limits need to be applied to one over the other. @@ -444,6 +450,7 @@ The final configuration looks like: "DynamicRoutes": [ { "ServiceName": "product", + "ServiceNamespace": "", // not supported for Consul, but supported for Kubernetes "RateLimitOptions": { "Limit": 5, "Period": "1s", @@ -454,6 +461,9 @@ The final configuration looks like: "ServiceName": "notification", "RateLimitOptions": { "EnableRateLimiting": false // notification service is unlimited! + }, + "LoadBalancerOptions": { + "Type": "LeastConnection" // switch from RoundRobin to LeastConnection } } ], @@ -463,7 +473,8 @@ The final configuration looks like: "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500, - "Type": "Consul" + "Type": "Consul", + "Namespace": "" // not supported for Consul, but supported for Kubernetes }, "RateLimitOptions": { "ClientIdHeader": "Oc-DynamicRouting-Client", @@ -472,6 +483,9 @@ The final configuration looks like: "Period": "10s", // fixed window "QuotaExceededMessage": "No Quota!", "HttpStatusCode": 499 // special shared status + }, + "LoadBalancerOptions": { + "Type": "RoundRobin" } } } @@ -486,6 +500,11 @@ The 'notification' service is unlimited because rate limiting is disabled. All o Use ``RateLimitOptions`` instead of ``RateLimitRule``! Note that ``RateLimitRule`` will be removed in version `25.0`_! For backward compatibility in version `24.1`_, the ``RateLimitRule`` section takes precedence over the ``RateLimitOptions`` section. +.. _break: http://break.do + + **Note**: The ``ServiceNamespace`` option was introduced in version `24.1`_ to enable precise overrides for the :doc:`../features/kubernetes` providers. + If ``ServiceNamespace`` is left empty or undefined, only one dynamic route with the same ``ServiceName`` may be defined in the ``DynamicRoutes`` collection. + .. _sd-custom-providers: Custom Providers @@ -542,8 +561,7 @@ Finally, in the `Program`_, register a ``ServiceDiscoveryFinderDelegate`` to ini Sample ------ -In order to introduce a basic template for a custom Service Discovery provider, we've prepared a good sample: -To provide a basic template for a custom *Service Discovery* provider, we have prepared a sample: +To offer a basic template for a :ref:`sd-custom-providers`, we have created a sample: | Project: `samples `_ / `ServiceDiscovery `_ | Solution: `Ocelot.Samples.ServiceDiscovery.sln `_ diff --git a/samples/Metadata/ocelot.json b/samples/Metadata/ocelot.json index 67762decc..df7c693c5 100644 --- a/samples/Metadata/ocelot.json +++ b/samples/Metadata/ocelot.json @@ -161,11 +161,11 @@ ], "GlobalConfiguration": { "BaseUrl": "http://localhost:5139", + "Metadata": { + "app-name": "Ocelot Metadata sample" + }, "MetadataOptions": { - "CurrentCulture": "en-GB", - "Metadata": { - "app-name": "Ocelot Metadata sample" - } + "CurrentCulture": "en-GB" } } } diff --git a/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs b/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs index 36d1cbddd..2f0747411 100644 --- a/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs +++ b/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs @@ -40,7 +40,7 @@ protected virtual string GetServiceName(KubeRegistryConfiguration configuration, protected virtual ServiceHostAndPort GetServiceHostAndPort(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) { var ports = subset.Ports; - bool portNameToScheme(EndpointPortV1 p) => string.Equals(p.Name, configuration.Scheme, StringComparison.InvariantCultureIgnoreCase); + bool portNameToScheme(EndpointPortV1 p) => string.Equals(p.Name, configuration.Scheme, StringComparison.OrdinalIgnoreCase); var portV1 = string.IsNullOrEmpty(configuration.Scheme) || !ports.Any(portNameToScheme) ? ports.FirstOrDefault() : ports.FirstOrDefault(portNameToScheme); diff --git a/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs b/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs index 6a03436b8..8752ae141 100644 --- a/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs +++ b/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs @@ -56,7 +56,7 @@ public ResiliencePipeline GetResiliencePipeline(DownstreamR } return _registry.GetOrAddPipeline( - key: new OcelotResiliencePipelineKey(GetRouteName(route)), + key: new OcelotResiliencePipelineKey(route.LoadBalancerKey), configure: (builder) => ConfigureStrategies(builder, route)); } diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index abfbe78f3..8d7d493e2 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -1,4 +1,5 @@ using Ocelot.Configuration.Creator; +using Ocelot.Infrastructure.Extensions; using Ocelot.Values; namespace Ocelot.Configuration.Builder; @@ -9,7 +10,7 @@ public class DownstreamRouteBuilder private string _loadBalancerKey; private string _downstreamPathTemplate; private UpstreamPathTemplate _upstreamTemplatePattern; - private List _upstreamHttpMethod; + private HashSet _upstreamHttpMethod; private bool _isAuthenticated; private List _claimsToHeaders; private List _claimToClaims; @@ -88,11 +89,9 @@ public DownstreamRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate inpu return this; } - public DownstreamRouteBuilder WithUpstreamHttpMethod(List input) + public DownstreamRouteBuilder WithUpstreamHttpMethod(IEnumerable methods) { - _upstreamHttpMethod = input.Count > 0 - ? input.Select(x => new HttpMethod(x.Trim())).ToList() - : new(); + _upstreamHttpMethod = methods.ToHttpMethods(); return this; } diff --git a/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs index 282a7a46b..dbe3e59e0 100644 --- a/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs +++ b/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs @@ -20,12 +20,6 @@ public QoSOptionsBuilder WithTimeoutValue(int? value) return this; } - public QoSOptionsBuilder WithKey(string value) - { - Key = value; - return this; - } - public QoSOptionsBuilder WithFailureRatio(double? value) { FailureRatio = value; diff --git a/src/Ocelot/Configuration/Builder/RouteBuilder.cs b/src/Ocelot/Configuration/Builder/RouteBuilder.cs deleted file mode 100644 index d89b6fbd0..000000000 --- a/src/Ocelot/Configuration/Builder/RouteBuilder.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Ocelot.Configuration.File; -using Ocelot.Values; - -namespace Ocelot.Configuration.Builder; - -public class RouteBuilder -{ - private UpstreamPathTemplate _upstreamTemplatePattern; - private IList _upstreamHttpMethod; - private string _upstreamHost; - private List _downstreamRoutes; - private List _downstreamRoutesConfig; - private string _aggregator; - private IDictionary _upstreamHeaders; - - public RouteBuilder() - { - _downstreamRoutes = new List(); - _downstreamRoutesConfig = new List(); - } - - public RouteBuilder WithDownstreamRoute(DownstreamRoute value) - { - _downstreamRoutes.Add(value); - return this; - } - - public RouteBuilder WithDownstreamRoutes(List value) - { - _downstreamRoutes = value; - return this; - } - - public RouteBuilder WithUpstreamHost(string upstreamAddresses) - { - _upstreamHost = upstreamAddresses; - return this; - } - - public RouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate input) - { - _upstreamTemplatePattern = input; - return this; - } - - public RouteBuilder WithUpstreamHttpMethod(List input) - { - _upstreamHttpMethod = (input.Count == 0) ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); - return this; - } - - public RouteBuilder WithAggregateRouteConfig(List aggregateRouteConfigs) - { - _downstreamRoutesConfig = aggregateRouteConfigs; - return this; - } - - public RouteBuilder WithAggregator(string aggregator) - { - _aggregator = aggregator; - return this; - } - - public RouteBuilder WithUpstreamHeaders(IDictionary upstreamHeaders) - { - _upstreamHeaders = upstreamHeaders; - return this; - } - - public Route Build() - { - return new Route( - _downstreamRoutes, - _downstreamRoutesConfig, - _upstreamHttpMethod, - _upstreamTemplatePattern, - _upstreamHost, - _aggregator, - _upstreamHeaders); - } -} diff --git a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs index 8d0637ca0..8b77c5339 100644 --- a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs +++ b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs @@ -1,5 +1,5 @@ -using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; +using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Creator; @@ -39,16 +39,16 @@ private Route SetUpAggregateRoute(IEnumerable routes, FileAggregateRoute var upstreamTemplatePattern = _creator.Create(aggregateRoute); var upstreamHeaderTemplates = _headerCreator.Create(aggregateRoute); - var upstreamHttpMethod = (aggregateRoute.UpstreamHttpMethod.Count == 0) ? new List() - : aggregateRoute.UpstreamHttpMethod.Select(x => new HttpMethod(x.Trim())).ToList(); - - return new Route( - applicableRoutes, - aggregateRoute.RouteKeysConfig, - upstreamHttpMethod, - upstreamTemplatePattern, - aggregateRoute.UpstreamHost, - aggregateRoute.Aggregator, - upstreamHeaderTemplates); + var upstreamHttpMethod = aggregateRoute.UpstreamHttpMethod.ToHttpMethods(); + return new Route() + { + Aggregator = aggregateRoute.Aggregator, + DownstreamRoute = applicableRoutes, + DownstreamRouteConfig = aggregateRoute.RouteKeysConfig, + UpstreamHeaderTemplates = upstreamHeaderTemplates, + UpstreamHost = aggregateRoute.UpstreamHost, + UpstreamHttpMethod = upstreamHttpMethod, + UpstreamTemplatePattern = upstreamTemplatePattern, + }; } } diff --git a/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs index 6d1c8f2b4..077bec8fc 100644 --- a/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs @@ -4,7 +4,7 @@ namespace Ocelot.Configuration.Creator; public class CacheOptionsCreator : ICacheOptionsCreator { - public CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration global, string upstreamPathTemplate, IList upstreamHttpMethods) + public CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration global, string upstreamPathTemplate, IReadOnlyCollection upstreamHttpMethods) { var region = GetRegion(options.Region ?? global?.CacheOptions.Region, upstreamPathTemplate, upstreamHttpMethods); var header = options.Header ?? global?.CacheOptions.Header; @@ -14,7 +14,7 @@ public CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration glo return new CacheOptions(ttlSeconds, region, header, enableContentHashing); } - protected virtual string GetRegion(string region, string upstreamPathTemplate, IList upstreamHttpMethod) + protected virtual string GetRegion(string region, string upstreamPathTemplate, IReadOnlyCollection upstreamHttpMethod) { if (!string.IsNullOrEmpty(region)) { diff --git a/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs index e7d323a8b..6d2db5a17 100644 --- a/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs @@ -13,6 +13,8 @@ public class ConfigurationCreator : IConfigurationCreator private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; private readonly IVersionCreator _versionCreator; private readonly IVersionPolicyCreator _versionPolicyCreator; + private readonly IMetadataCreator _metadataCreator; + private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; public ConfigurationCreator( IServiceProviderConfigurationCreator serviceProviderConfigCreator, @@ -21,8 +23,9 @@ public ConfigurationCreator( IServiceProvider serviceProvider, ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IVersionCreator versionCreator, - IVersionPolicyCreator versionPolicyCreator - ) + IVersionPolicyCreator versionPolicyCreator, + IMetadataCreator metadataCreator, + IRateLimitOptionsCreator rateLimitOptionsCreator) { _adminPath = serviceProvider.GetService(); _loadBalancerOptionsCreator = loadBalancerOptionsCreator; @@ -30,35 +33,36 @@ IVersionPolicyCreator versionPolicyCreator _qosOptionsCreator = qosOptionsCreator; _httpHandlerOptionsCreator = httpHandlerOptionsCreator; _versionCreator = versionCreator; - _versionPolicyCreator = versionPolicyCreator; + _versionPolicyCreator = versionPolicyCreator; + _metadataCreator = metadataCreator; + _rateLimitOptionsCreator = rateLimitOptionsCreator; } - public InternalConfiguration Create(FileConfiguration fileConfiguration, List routes) + public InternalConfiguration Create(FileConfiguration configuration, List routes) { - var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(fileConfiguration.GlobalConfiguration); - - var lbOptions = _loadBalancerOptionsCreator.Create(fileConfiguration.GlobalConfiguration.LoadBalancerOptions); - - var qosOptions = _qosOptionsCreator.Create(fileConfiguration.GlobalConfiguration.QoSOptions); - - var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileConfiguration.GlobalConfiguration.HttpHandlerOptions); - var adminPath = _adminPath?.Path; - - var version = _versionCreator.Create(fileConfiguration.GlobalConfiguration.DownstreamHttpVersion); - - var versionPolicy = _versionPolicyCreator.Create(fileConfiguration.GlobalConfiguration.DownstreamHttpVersionPolicy); + var globalConfiguration = configuration.GlobalConfiguration ?? new(); + var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(globalConfiguration); + var lbOptions = _loadBalancerOptionsCreator.Create(globalConfiguration.LoadBalancerOptions); + var qosOptions = _qosOptionsCreator.Create(globalConfiguration.QoSOptions); + var httpHandlerOptions = _httpHandlerOptionsCreator.Create(globalConfiguration.HttpHandlerOptions); + var version = _versionCreator.Create(globalConfiguration.DownstreamHttpVersion); + var versionPolicy = _versionPolicyCreator.Create(globalConfiguration.DownstreamHttpVersionPolicy); + var metadataOptions = _metadataCreator.Create(null, globalConfiguration); + var rateLimitOptions = _rateLimitOptionsCreator.Create(globalConfiguration); return new InternalConfiguration(routes, adminPath, serviceProviderConfiguration, - fileConfiguration.GlobalConfiguration.RequestIdKey, + globalConfiguration.RequestIdKey, lbOptions, - fileConfiguration.GlobalConfiguration.DownstreamScheme, + globalConfiguration.DownstreamScheme, qosOptions, httpHandlerOptions, version, - versionPolicy - ); + versionPolicy, + metadataOptions, + rateLimitOptions, + globalConfiguration.Timeout); } } diff --git a/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs b/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs index 4ffcafe67..9b09f1f1c 100644 --- a/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs @@ -10,20 +10,17 @@ public class DefaultMetadataCreator : IMetadataCreator { public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration globalConfiguration) { - // metadata from the route could be null when no metadata is defined metadata ??= new Dictionary(); - - // metadata from the global configuration is never null - var options = globalConfiguration.MetadataOptions; - var mergedMetadata = new Dictionary(options.Metadata); - + globalConfiguration.Metadata ??= new Dictionary(); + var merged = new Dictionary(globalConfiguration.Metadata); foreach (var (key, value) in metadata) { - mergedMetadata[key] = value; + merged[key] = value; } + var options = globalConfiguration.MetadataOptions; return new MetadataOptionsBuilder() - .WithMetadata(mergedMetadata) + .WithMetadata(merged) .WithSeparators(options.Separators) .WithTrimChars(options.TrimChars) .WithStringSplitOption(options.StringSplitOption) diff --git a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs b/src/Ocelot/Configuration/Creator/DynamicRoutesCreator.cs similarity index 50% rename from src/Ocelot/Configuration/Creator/DynamicsCreator.cs rename to src/Ocelot/Configuration/Creator/DynamicRoutesCreator.cs index c4e1f68ee..be5ce5781 100644 --- a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs +++ b/src/Ocelot/Configuration/Creator/DynamicRoutesCreator.cs @@ -1,70 +1,82 @@ using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; - -namespace Ocelot.Configuration.Creator; - -public class DynamicsCreator : IDynamicsCreator -{ - private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; - private readonly IVersionCreator _versionCreator; - private readonly IVersionPolicyCreator _versionPolicyCreator; + +namespace Ocelot.Configuration.Creator; + +public class DynamicRoutesCreator : IDynamicsCreator +{ + private readonly IRouteKeyCreator _loadBalancerKeyCreator; + private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; private readonly IMetadataCreator _metadataCreator; - - public DynamicsCreator( - IRateLimitOptionsCreator rateLimitOptionsCreator, - IVersionCreator versionCreator, - IVersionPolicyCreator versionPolicyCreator, - IMetadataCreator metadataCreator) - { - _rateLimitOptionsCreator = rateLimitOptionsCreator; - _versionCreator = versionCreator; - _versionPolicyCreator = versionPolicyCreator; + private readonly IQoSOptionsCreator _qosOptionsCreator; + private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; + private readonly IVersionCreator _versionCreator; + private readonly IVersionPolicyCreator _versionPolicyCreator; + + public DynamicRoutesCreator( + IRouteKeyCreator loadBalancerKeyCreator, + ILoadBalancerOptionsCreator loadBalancerOptionsCreator, + IMetadataCreator metadataCreator, + IQoSOptionsCreator qosOptionsCreator, + IRateLimitOptionsCreator rateLimitOptionsCreator, + IVersionCreator versionCreator, + IVersionPolicyCreator versionPolicyCreator) + { + _loadBalancerKeyCreator = loadBalancerKeyCreator; + _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _metadataCreator = metadataCreator; - } - - public IReadOnlyList Create(FileConfiguration fileConfiguration) - { - return fileConfiguration.DynamicRoutes - .Select(dynamic => SetUpDynamicRoute(dynamic, fileConfiguration.GlobalConfiguration)) - .ToList(); - } - - public virtual int CreateTimeout(FileDynamicRoute route, FileGlobalConfiguration global) - { - int def = DownstreamRoute.DefaultTimeoutSeconds; - return route.Timeout.Positive(def) ?? global.Timeout.Positive(def) ?? def; - } - - private Route SetUpDynamicRoute(FileDynamicRoute dynamicRoute, FileGlobalConfiguration globalConfiguration) - { - // The old RateLimitRule property takes precedence over the new RateLimitOptions property for backward compatibility, thus, override forcibly - if (dynamicRoute.RateLimitRule != null) - { - dynamicRoute.RateLimitOptions = dynamicRoute.RateLimitRule; - } - - var rateLimitOptions = _rateLimitOptionsCreator.Create(dynamicRoute, globalConfiguration); - var version = _versionCreator.Create(dynamicRoute.DownstreamHttpVersion); - var versionPolicy = _versionPolicyCreator.Create(dynamicRoute.DownstreamHttpVersionPolicy); + _qosOptionsCreator = qosOptionsCreator; + _rateLimitOptionsCreator = rateLimitOptionsCreator; + _versionCreator = versionCreator; + _versionPolicyCreator = versionPolicyCreator; + } + + public IReadOnlyList Create(FileConfiguration fileConfiguration) + { + Route CreateRoute(FileDynamicRoute route) + => SetUpDynamicRoute(route, fileConfiguration.GlobalConfiguration); + return fileConfiguration.DynamicRoutes + .Select(CreateRoute) + .ToArray(); + } + + public virtual int CreateTimeout(FileDynamicRoute route, FileGlobalConfiguration global) + { + int def = DownstreamRoute.DefaultTimeoutSeconds; + return route.Timeout.Positive(def) ?? global.Timeout.Positive(def) ?? def; + } + + private Route SetUpDynamicRoute(FileDynamicRoute dynamicRoute, FileGlobalConfiguration globalConfiguration) + { + // The old RateLimitRule property takes precedence over the new RateLimitOptions property for backward compatibility, thus, override forcibly + if (dynamicRoute.RateLimitRule != null) + { + dynamicRoute.RateLimitOptions = dynamicRoute.RateLimitRule; + } + + var version = _versionCreator.Create(dynamicRoute.DownstreamHttpVersion.IfEmpty(globalConfiguration.DownstreamHttpVersion)); + var versionPolicy = _versionPolicyCreator.Create(dynamicRoute.DownstreamHttpVersionPolicy.IfEmpty(globalConfiguration.DownstreamHttpVersionPolicy)); + var scheme = dynamicRoute.DownstreamScheme.IfEmpty(globalConfiguration.DownstreamScheme); + var lbOptions = _loadBalancerOptionsCreator.Create(dynamicRoute, globalConfiguration); + var lbKey = _loadBalancerKeyCreator.Create(dynamicRoute, lbOptions); var metadata = _metadataCreator.Create(dynamicRoute.Metadata, globalConfiguration); - - var downstreamRoute = new DownstreamRouteBuilder() - .WithRateLimitOptions(rateLimitOptions) - .WithServiceName(dynamicRoute.ServiceName) - .WithDownstreamHttpVersion(version) + var qosOptions = _qosOptionsCreator.Create(dynamicRoute, globalConfiguration); + var rlOptions = _rateLimitOptionsCreator.Create(dynamicRoute, globalConfiguration); + var timeout = CreateTimeout(dynamicRoute, globalConfiguration); + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamHttpVersion(version) .WithDownstreamHttpVersionPolicy(versionPolicy) - .WithMetadata(metadata) - .WithTimeout(CreateTimeout(dynamicRoute, globalConfiguration)) - .Build(); - - return new Route( - new() { downstreamRoute }, - new(), - new List(), - upstreamTemplatePattern: default, - upstreamHost: default, - aggregator: default, - upstreamHeaderTemplates: default); - } -} + .WithDownstreamScheme(scheme) + .WithLoadBalancerKey(lbKey) + .WithLoadBalancerOptions(lbOptions) + .WithMetadata(metadata) + .WithQosOptions(qosOptions) + .WithRateLimitOptions(rlOptions) + .WithServiceName(dynamicRoute.ServiceName) + .WithServiceNamespace(dynamicRoute.ServiceNamespace) + .WithTimeout(timeout) + .Build(); + return new Route(true, downstreamRoute); // IsDynamic -> true + } +} diff --git a/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs b/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs index a76a1b20e..29e19fb37 100644 --- a/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs @@ -15,5 +15,5 @@ public interface ICacheOptionsCreator /// The upstream path template as string. /// The upstream http methods as a list of strings. /// The generated cache options. - CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration global, string upstreamPathTemplate, IList upstreamHttpMethods); + CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration global, string upstreamPathTemplate, IReadOnlyCollection upstreamHttpMethods); } diff --git a/src/Ocelot/Configuration/Creator/IConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IConfigurationCreator.cs index 50e3e29fd..8144244c8 100644 --- a/src/Ocelot/Configuration/Creator/IConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/IConfigurationCreator.cs @@ -4,5 +4,5 @@ namespace Ocelot.Configuration.Creator; public interface IConfigurationCreator { - InternalConfiguration Create(FileConfiguration fileConfiguration, List routes); + InternalConfiguration Create(FileConfiguration configuration, List routes); } diff --git a/src/Ocelot/Configuration/Creator/ILoadBalancerOptionsCreator.cs b/src/Ocelot/Configuration/Creator/ILoadBalancerOptionsCreator.cs index d7fe7f61c..23f524046 100644 --- a/src/Ocelot/Configuration/Creator/ILoadBalancerOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/ILoadBalancerOptionsCreator.cs @@ -5,4 +5,6 @@ namespace Ocelot.Configuration.Creator; public interface ILoadBalancerOptionsCreator { LoadBalancerOptions Create(FileLoadBalancerOptions options); + LoadBalancerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration); + LoadBalancerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration); } diff --git a/src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs b/src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs index 7f6ee76ea..43491b5c2 100644 --- a/src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs @@ -5,6 +5,6 @@ namespace Ocelot.Configuration.Creator; public interface IQoSOptionsCreator { QoSOptions Create(FileQoSOptions options); - QoSOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration); + QoSOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration); } diff --git a/src/Ocelot/Configuration/Creator/IRateLimitOptionsCreator.cs b/src/Ocelot/Configuration/Creator/IRateLimitOptionsCreator.cs index 57b421205..099a0ba0f 100644 --- a/src/Ocelot/Configuration/Creator/IRateLimitOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/IRateLimitOptionsCreator.cs @@ -4,5 +4,6 @@ namespace Ocelot.Configuration.Creator; public interface IRateLimitOptionsCreator { + RateLimitOptions Create(FileGlobalConfiguration globalConfiguration); RateLimitOptions Create(IRouteRateLimiting route, FileGlobalConfiguration globalConfiguration); } diff --git a/src/Ocelot/Configuration/Creator/IRouteKeyCreator.cs b/src/Ocelot/Configuration/Creator/IRouteKeyCreator.cs index 07ef8be17..4865a096a 100644 --- a/src/Ocelot/Configuration/Creator/IRouteKeyCreator.cs +++ b/src/Ocelot/Configuration/Creator/IRouteKeyCreator.cs @@ -4,5 +4,7 @@ namespace Ocelot.Configuration.Creator; public interface IRouteKeyCreator { - string Create(FileRoute fileRoute); + string Create(FileRoute route, LoadBalancerOptions loadBalancing); + string Create(FileDynamicRoute route, LoadBalancerOptions loadBalancing); + string Create(string serviceNamespace, string serviceName, LoadBalancerOptions loadBalancing); } diff --git a/src/Ocelot/Configuration/Creator/LoadBalancerOptionsCreator.cs b/src/Ocelot/Configuration/Creator/LoadBalancerOptionsCreator.cs index 1aafeb489..cd7772fe9 100644 --- a/src/Ocelot/Configuration/Creator/LoadBalancerOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/LoadBalancerOptionsCreator.cs @@ -1,15 +1,64 @@ using Ocelot.Configuration.File; +using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Creator; public class LoadBalancerOptionsCreator : ILoadBalancerOptionsCreator { public LoadBalancerOptions Create(FileLoadBalancerOptions options) + => new(options); + + public LoadBalancerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration) + { + ArgumentNullException.ThrowIfNull(route); + ArgumentNullException.ThrowIfNull(globalConfiguration); + return Create(route, route.LoadBalancerOptions, globalConfiguration.LoadBalancerOptions); + } + + public LoadBalancerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration) + { + ArgumentNullException.ThrowIfNull(route); + ArgumentNullException.ThrowIfNull(globalConfiguration); + return Create(route, route.LoadBalancerOptions, globalConfiguration.LoadBalancerOptions); + } + + protected virtual LoadBalancerOptions Create(IRouteGrouping grouping, FileLoadBalancerOptions options, FileGlobalLoadBalancerOptions globalOptions) + { + ArgumentNullException.ThrowIfNull(grouping); + var group = globalOptions; + bool isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global + group.RouteKeys.Count == 0 || // empty collection -> is global + group.RouteKeys.Contains(grouping.Key); // this route is in the group + + if (options == null && globalOptions != null && isGlobal) + { + return new(globalOptions); + } + + if (options != null && globalOptions == null) + { + return new(options); + } + else if (options != null && globalOptions != null && !isGlobal) + { + return new(options); + } + + if (options != null && globalOptions != null && isGlobal) + { + return Merge(options, globalOptions); + } + + return new(); + } + + protected virtual LoadBalancerOptions Merge(FileLoadBalancerOptions options, FileLoadBalancerOptions globalOptions) { - return new LoadBalancerOptionsBuilder() - .WithType(options.Type) - .WithKey(options.Key) - .WithExpiryInMs(options.Expiry) - .Build(); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(globalOptions); + options.Type = options.Type.IfEmpty(globalOptions.Type); + options.Key = options.Key.IfEmpty(globalOptions.Key); + options.Expiry ??= globalOptions.Expiry; + return new(options); } } diff --git a/src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs b/src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs index 41fd6fd5a..b276ad1e1 100644 --- a/src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs @@ -4,16 +4,21 @@ namespace Ocelot.Configuration.Creator; public class QoSOptionsCreator : IQoSOptionsCreator { - public QoSOptions Create(FileQoSOptions options) => new(options); - + public QoSOptions Create(FileQoSOptions options) + => new(options); public QoSOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration) + => Merge(route.QoSOptions, globalConfiguration.QoSOptions); + public QoSOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration) + => Merge(route.QoSOptions, globalConfiguration.QoSOptions); + + protected virtual QoSOptions Merge(FileQoSOptions options, FileQoSOptions global) { - FileQoSOptions qos = route.QoSOptions, global = globalConfiguration.QoSOptions; - qos.DurationOfBreak ??= global.DurationOfBreak; - qos.ExceptionsAllowedBeforeBreaking ??= global.ExceptionsAllowedBeforeBreaking; - qos.FailureRatio ??= global.FailureRatio; - qos.SamplingDuration ??= global.SamplingDuration; - qos.TimeoutValue ??= global.TimeoutValue; - return new(qos); + options ??= new(); + options.DurationOfBreak ??= global.DurationOfBreak; + options.ExceptionsAllowedBeforeBreaking ??= global.ExceptionsAllowedBeforeBreaking; + options.FailureRatio ??= global.FailureRatio; + options.SamplingDuration ??= global.SamplingDuration; + options.TimeoutValue ??= global.TimeoutValue; + return new(options); } } diff --git a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs index 69193f3aa..4decfb502 100644 --- a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs @@ -5,9 +5,12 @@ namespace Ocelot.Configuration.Creator; public class RateLimitOptionsCreator : IRateLimitOptionsCreator { - public RateLimitOptionsCreator() - { - } + public RateLimitOptionsCreator() { } + + public RateLimitOptions Create(FileGlobalConfiguration globalConfiguration) + => globalConfiguration.RateLimitOptions != null + ? new(globalConfiguration.RateLimitOptions) + : new(false); public RateLimitOptions Create(IRouteRateLimiting route, FileGlobalConfiguration globalConfiguration) { @@ -16,11 +19,12 @@ public RateLimitOptions Create(IRouteRateLimiting route, FileGlobalConfiguration var rule = route.RateLimitOptions; var globalOptions = globalConfiguration.RateLimitOptions; + var group = globalOptions as IRouteGroup; // bool isGlobal = globalOptions?.RouteKeys?.Contains(route.Key) ?? true; - bool isGlobal = globalOptions?.RouteKeys is null || // undefined section or array option -> is global - globalOptions.RouteKeys.Count == 0 || // empty collection -> is global - globalOptions.RouteKeys.Contains(route.Key); // this route is in the group + bool isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global + group.RouteKeys.Count == 0 || // empty collection -> is global + group.RouteKeys.Contains(route.Key); // this route is in the group if (rule?.EnableRateLimiting == false || (isGlobal && globalOptions?.EnableRateLimiting == false)) { @@ -46,7 +50,7 @@ public RateLimitOptions Create(IRouteRateLimiting route, FileGlobalConfiguration return new(false); } - protected virtual RateLimitOptions MergeHeaderRules(FileRateLimitByHeaderRule rule, FileGlobalRateLimitByHeaderRule globalRule) + protected virtual RateLimitOptions MergeHeaderRules(FileRateLimitByHeaderRule rule, FileRateLimitByHeaderRule globalRule) { ArgumentNullException.ThrowIfNull(rule); ArgumentNullException.ThrowIfNull(globalRule); diff --git a/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs b/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs index 8bc5debc9..7da20b86c 100644 --- a/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs +++ b/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs @@ -1,76 +1,82 @@ using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.Infrastructure.Extensions; +using Ocelot.LoadBalancer.Balancers; namespace Ocelot.Configuration.Creator; public class RouteKeyCreator : IRouteKeyCreator -{ +{ + public const char Separator = '|'; + public const char Dot = DiscoveryDownstreamRouteFinder.Dot; + /// /// Creates the unique key based on the route properties for load balancing etc. /// /// - /// Key template: - /// - /// UpstreamHttpMethod|UpstreamPathTemplate|UpstreamHost|DownstreamHostAndPorts|ServiceNamespace|ServiceName|LoadBalancerType|LoadBalancerKey - /// - /// - /// The route object. + /// Key template: UpstreamHttpMethod|UpstreamPathTemplate|UpstreamHost|DownstreamHostAndPorts|ServiceNamespace|ServiceName|LoadBalancerType|LoadBalancerKey. + /// + /// The route object. + /// Final options for load balancing. /// A object containing the key. - public string Create(FileRoute fileRoute) + public string Create(FileRoute route, LoadBalancerOptions loadBalancing) { - var isStickySession = fileRoute.LoadBalancerOptions is + if (TryStickySession(loadBalancing, out var stickySessionKey)) { - Type: nameof(CookieStickySessions), - Key.Length: > 0 - }; + return stickySessionKey; + } - if (isStickySession) + var keyBuilder = new StringBuilder() + .AppendNext(route.UpstreamHttpMethod.Csv()) // required + .AppendNext(route.UpstreamPathTemplate) // required + .AppendNext(route.UpstreamHost.IfEmpty("no-host")) // optional... + .AppendNext(route.DownstreamHostAndPorts.Select(AsString).Csv().IfEmpty("no-host-and-port")) + .AppendNext(route.ServiceNamespace.IfEmpty("no-svc-ns")) + .AppendNext(route.ServiceName.IfEmpty("no-svc-name")) + .AppendNext(loadBalancing.Type.IfEmpty("no-lb-type")) + .AppendNext(loadBalancing.Key.IfEmpty("no-lb-key")); + return keyBuilder.ToString(); + } + + public string Create(FileDynamicRoute route, LoadBalancerOptions loadBalancing) + { + if (TryStickySession(loadBalancing, out var stickySessionKey)) { - return $"{nameof(CookieStickySessions)}:{fileRoute.LoadBalancerOptions.Key}"; + return stickySessionKey; } - var upstreamHttpMethods = Csv(fileRoute.UpstreamHttpMethod); - var downstreamHostAndPorts = Csv(fileRoute.DownstreamHostAndPorts.Select(downstream => $"{downstream.Host}:{downstream.Port}")); - - var keyBuilder = new StringBuilder() - - // UpstreamHttpMethod and UpstreamPathTemplate are required - .AppendNext(upstreamHttpMethods) - .AppendNext(fileRoute.UpstreamPathTemplate) - - // Other properties are optional, replace undefined values with defaults to aid debugging - .AppendNext(Coalesce(fileRoute.UpstreamHost, "no-host")) + // it should be constructed in upper contexts + return !loadBalancing.Key.IsEmpty() ? loadBalancing.Key + : Create(route.ServiceNamespace, route.ServiceName, loadBalancing); + } - .AppendNext(Coalesce(downstreamHostAndPorts, "no-host-and-port")) - .AppendNext(Coalesce(fileRoute.ServiceNamespace, "no-svc-ns")) - .AppendNext(Coalesce(fileRoute.ServiceName, "no-svc-name")) - .AppendNext(Coalesce(fileRoute.LoadBalancerOptions.Type, "no-lb-type")) - .AppendNext(Coalesce(fileRoute.LoadBalancerOptions.Key, "no-lb-key")); + public string Create(string serviceNamespace, string serviceName, LoadBalancerOptions loadBalancing) + { + if (TryStickySession(loadBalancing, out var stickySessionKey)) + { + return stickySessionKey; + } - return keyBuilder.ToString(); - } - - /// - /// Helper function to convert multiple strings into a comma-separated string. - /// - /// The collection of strings to join by comma separator. - /// A in the comma-separated format. - private static string Csv(IEnumerable values) => string.Join(',', values); - - /// - /// Helper function to return the first non-null-or-whitespace string. - /// - /// The 1st string to check. - /// The 2nd string to check. - /// A which is not empty. - private static string Coalesce(string first, string second) => string.IsNullOrWhiteSpace(first) ? second : first; + return !loadBalancing.Key.IsEmpty() ? loadBalancing.Key + : string.Join(Dot, serviceNamespace, serviceName); // upstreamHttpMethod ? + } + + protected virtual bool TryStickySession(LoadBalancerOptions loadBalancing, out string stickySessionKey) + { + bool isStickySession = nameof(CookieStickySessions).Equals(loadBalancing.Type, StringComparison.OrdinalIgnoreCase) + && loadBalancing.Key.Length > 0; + stickySessionKey = isStickySession + ? $"{nameof(CookieStickySessions)}:{loadBalancing.Key}" + : string.Empty; + return isStickySession; + } + + public static string AsString(FileHostAndPort host) => host?.ToString(); } internal static class RouteKeyCreatorHelpers { - /// - /// Helper function to append a string to the key builder, separated by a pipe. - /// + /// Helper function to append a string to the key builder, separated by a pipe. /// The builder of the key. /// The next word to add. /// The reference to the builder. @@ -78,7 +84,7 @@ public static StringBuilder AppendNext(this StringBuilder builder, string next) { if (builder.Length > 0) { - builder.Append('|'); + builder.Append(RouteKeyCreator.Separator); } return builder.Append(next); diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/StaticRoutesCreator.cs similarity index 90% rename from src/Ocelot/Configuration/Creator/RoutesCreator.cs rename to src/Ocelot/Configuration/Creator/StaticRoutesCreator.cs index acfb57d0c..8440c3b04 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/StaticRoutesCreator.cs @@ -1,184 +1,180 @@ using Ocelot.Configuration.Builder; -using Ocelot.Configuration.File; -using Ocelot.Infrastructure.Extensions; - -namespace Ocelot.Configuration.Creator; - -public class RoutesCreator : IRoutesCreator // TODO: Rename to StaticRoutesCreator -{ - private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; - private readonly IClaimsToThingCreator _claimsToThingCreator; - private readonly IAuthenticationOptionsCreator _authOptionsCreator; - private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator; - private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator; - private readonly IRequestIdKeyCreator _requestIdKeyCreator; - private readonly IQoSOptionsCreator _qosOptionsCreator; - private readonly IRouteOptionsCreator _fileRouteOptionsCreator; - private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; - private readonly ICacheOptionsCreator _cacheOptionsCreator; - private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; - private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; - private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; - private readonly IRouteKeyCreator _routeKeyCreator; - private readonly ISecurityOptionsCreator _securityOptionsCreator; - private readonly IVersionCreator _versionCreator; +using Ocelot.Configuration.File; +using Ocelot.Infrastructure.Extensions; + +namespace Ocelot.Configuration.Creator; + +public class StaticRoutesCreator : IRoutesCreator +{ + private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; + private readonly IClaimsToThingCreator _claimsToThingCreator; + private readonly IAuthenticationOptionsCreator _authOptionsCreator; + private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator; + private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator; + private readonly IRequestIdKeyCreator _requestIdKeyCreator; + private readonly IQoSOptionsCreator _qosOptionsCreator; + private readonly IRouteOptionsCreator _fileRouteOptionsCreator; + private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; + private readonly ICacheOptionsCreator _cacheOptionsCreator; + private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; + private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; + private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; + private readonly IRouteKeyCreator _routeKeyCreator; + private readonly ISecurityOptionsCreator _securityOptionsCreator; + private readonly IVersionCreator _versionCreator; private readonly IVersionPolicyCreator _versionPolicyCreator; private readonly IMetadataCreator _metadataCreator; - - public RoutesCreator( - IClaimsToThingCreator claimsToThingCreator, - IAuthenticationOptionsCreator authOptionsCreator, - IUpstreamTemplatePatternCreator upstreamTemplatePatternCreator, - IRequestIdKeyCreator requestIdKeyCreator, - IQoSOptionsCreator qosOptionsCreator, - IRouteOptionsCreator fileRouteOptionsCreator, - IRateLimitOptionsCreator rateLimitOptionsCreator, - ICacheOptionsCreator cacheOptionsCreator, - IHttpHandlerOptionsCreator httpHandlerOptionsCreator, - IHeaderFindAndReplaceCreator headerFAndRCreator, - IDownstreamAddressesCreator downstreamAddressesCreator, - ILoadBalancerOptionsCreator loadBalancerOptionsCreator, - IRouteKeyCreator routeKeyCreator, - ISecurityOptionsCreator securityOptionsCreator, - IVersionCreator versionCreator, + + public StaticRoutesCreator( + IClaimsToThingCreator claimsToThingCreator, + IAuthenticationOptionsCreator authOptionsCreator, + IUpstreamTemplatePatternCreator upstreamTemplatePatternCreator, + IRequestIdKeyCreator requestIdKeyCreator, + IQoSOptionsCreator qosOptionsCreator, + IRouteOptionsCreator fileRouteOptionsCreator, + IRateLimitOptionsCreator rateLimitOptionsCreator, + ICacheOptionsCreator cacheOptionsCreator, + IHttpHandlerOptionsCreator httpHandlerOptionsCreator, + IHeaderFindAndReplaceCreator headerFAndRCreator, + IDownstreamAddressesCreator downstreamAddressesCreator, + ILoadBalancerOptionsCreator loadBalancerOptionsCreator, + IRouteKeyCreator routeKeyCreator, + ISecurityOptionsCreator securityOptionsCreator, + IVersionCreator versionCreator, IVersionPolicyCreator versionPolicyCreator, - IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator, - IMetadataCreator metadataCreator) - { - _routeKeyCreator = routeKeyCreator; - _loadBalancerOptionsCreator = loadBalancerOptionsCreator; - _downstreamAddressesCreator = downstreamAddressesCreator; - _headerFAndRCreator = headerFAndRCreator; - _cacheOptionsCreator = cacheOptionsCreator; - _rateLimitOptionsCreator = rateLimitOptionsCreator; - _requestIdKeyCreator = requestIdKeyCreator; - _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; - _authOptionsCreator = authOptionsCreator; - _claimsToThingCreator = claimsToThingCreator; - _qosOptionsCreator = qosOptionsCreator; - _fileRouteOptionsCreator = fileRouteOptionsCreator; - _httpHandlerOptionsCreator = httpHandlerOptionsCreator; - _loadBalancerOptionsCreator = loadBalancerOptionsCreator; - _securityOptionsCreator = securityOptionsCreator; - _versionCreator = versionCreator; + IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator, + IMetadataCreator metadataCreator) + { + _routeKeyCreator = routeKeyCreator; + _loadBalancerOptionsCreator = loadBalancerOptionsCreator; + _downstreamAddressesCreator = downstreamAddressesCreator; + _headerFAndRCreator = headerFAndRCreator; + _cacheOptionsCreator = cacheOptionsCreator; + _rateLimitOptionsCreator = rateLimitOptionsCreator; + _requestIdKeyCreator = requestIdKeyCreator; + _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; + _authOptionsCreator = authOptionsCreator; + _claimsToThingCreator = claimsToThingCreator; + _qosOptionsCreator = qosOptionsCreator; + _fileRouteOptionsCreator = fileRouteOptionsCreator; + _httpHandlerOptionsCreator = httpHandlerOptionsCreator; + _loadBalancerOptionsCreator = loadBalancerOptionsCreator; + _securityOptionsCreator = securityOptionsCreator; + _versionCreator = versionCreator; _versionPolicyCreator = versionPolicyCreator; - _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; + _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; _metadataCreator = metadataCreator; - } - - public IReadOnlyList Create(FileConfiguration fileConfiguration) - { - Route CreateRoute(FileRoute route) - => SetUpRoute(route, SetUpDownstreamRoute(route, fileConfiguration.GlobalConfiguration)); - return fileConfiguration.Routes - .Select(CreateRoute) - .ToList(); - } - - public virtual int CreateTimeout(FileRoute route, FileGlobalConfiguration global) - { - int def = DownstreamRoute.DefaultTimeoutSeconds; - return route.Timeout.Positive(def) ?? global.Timeout.Positive(def) ?? def; - } - - private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration) - { - var fileRouteOptions = _fileRouteOptionsCreator.Create(fileRoute, globalConfiguration); // TODO Refactor this overhead service by moving options to native creators - - var requestIdKey = _requestIdKeyCreator.Create(fileRoute, globalConfiguration); - - var routeKey = _routeKeyCreator.Create(fileRoute); - - var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); - - var authOptionsForRoute = _authOptionsCreator.Create(fileRoute, globalConfiguration); - - var claimsToHeaders = _claimsToThingCreator.Create(fileRoute.AddHeadersToRequest); - - var claimsToClaims = _claimsToThingCreator.Create(fileRoute.AddClaimsToRequest); - - var claimsToQueries = _claimsToThingCreator.Create(fileRoute.AddQueriesToRequest); - - var claimsToDownstreamPath = _claimsToThingCreator.Create(fileRoute.ChangeDownstreamPathTemplate); - - var qosOptions = _qosOptionsCreator.Create(fileRoute, globalConfiguration); - - var rateLimitOption = _rateLimitOptionsCreator.Create(fileRoute, globalConfiguration); - - var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileRoute.HttpHandlerOptions); - - var hAndRs = _headerFAndRCreator.Create(fileRoute, globalConfiguration); - - var downstreamAddresses = _downstreamAddressesCreator.Create(fileRoute); - - var lbOptions = _loadBalancerOptionsCreator.Create(fileRoute.LoadBalancerOptions); - - var securityOptions = _securityOptionsCreator.Create(fileRoute.SecurityOptions, globalConfiguration); - - var downstreamHttpVersion = _versionCreator.Create(fileRoute.DownstreamHttpVersion); - + } + + public IReadOnlyList Create(FileConfiguration fileConfiguration) + { + Route CreateRoute(FileRoute route) + => SetUpRoute(route, SetUpDownstreamRoute(route, fileConfiguration.GlobalConfiguration)); + return fileConfiguration.Routes + .Select(CreateRoute) + .ToArray(); + } + + public virtual int CreateTimeout(FileRoute route, FileGlobalConfiguration global) + { + int def = DownstreamRoute.DefaultTimeoutSeconds; + return route.Timeout.Positive(def) ?? global.Timeout.Positive(def) ?? def; + } + + private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration) + { + var fileRouteOptions = _fileRouteOptionsCreator.Create(fileRoute, globalConfiguration); // TODO Refactor this overhead service by moving options to native creators + + var requestIdKey = _requestIdKeyCreator.Create(fileRoute, globalConfiguration); + + var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); + + var authOptionsForRoute = _authOptionsCreator.Create(fileRoute, globalConfiguration); + + var claimsToHeaders = _claimsToThingCreator.Create(fileRoute.AddHeadersToRequest); + + var claimsToClaims = _claimsToThingCreator.Create(fileRoute.AddClaimsToRequest); + + var claimsToQueries = _claimsToThingCreator.Create(fileRoute.AddQueriesToRequest); + + var claimsToDownstreamPath = _claimsToThingCreator.Create(fileRoute.ChangeDownstreamPathTemplate); + + var qosOptions = _qosOptionsCreator.Create(fileRoute, globalConfiguration); + + var rateLimitOption = _rateLimitOptionsCreator.Create(fileRoute, globalConfiguration); + + var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileRoute.HttpHandlerOptions); + + var hAndRs = _headerFAndRCreator.Create(fileRoute, globalConfiguration); + + var downstreamAddresses = _downstreamAddressesCreator.Create(fileRoute); + + var lbOptions = _loadBalancerOptionsCreator.Create(fileRoute, globalConfiguration); + var lbKey = _routeKeyCreator.Create(fileRoute, lbOptions); + + var securityOptions = _securityOptionsCreator.Create(fileRoute.SecurityOptions, globalConfiguration); + + var downstreamHttpVersion = _versionCreator.Create(fileRoute.DownstreamHttpVersion); + var downstreamHttpVersionPolicy = _versionPolicyCreator.Create(fileRoute.DownstreamHttpVersionPolicy); - - var cacheOptions = _cacheOptionsCreator.Create(fileRoute.FileCacheOptions, globalConfiguration, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod); - + + var cacheOptions = _cacheOptionsCreator.Create(fileRoute.FileCacheOptions, globalConfiguration, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod); + var metadata = _metadataCreator.Create(fileRoute.Metadata, globalConfiguration); - - var route = new DownstreamRouteBuilder() - .WithKey(fileRoute.Key) - .WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate) - .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod.ToList()) - .WithUpstreamPathTemplate(upstreamTemplatePattern) - .WithIsAuthenticated(fileRouteOptions.IsAuthenticated) - .WithAuthenticationOptions(authOptionsForRoute) - .WithClaimsToHeaders(claimsToHeaders) - .WithClaimsToClaims(claimsToClaims) - .WithRouteClaimsRequirement(fileRoute.RouteClaimsRequirement) - .WithIsAuthorized(fileRouteOptions.IsAuthorized) - .WithClaimsToQueries(claimsToQueries) - .WithClaimsToDownstreamPath(claimsToDownstreamPath) - .WithRequestIdKey(requestIdKey) - .WithIsCached(fileRouteOptions.IsCached) - .WithCacheOptions(cacheOptions) - .WithDownstreamScheme(fileRoute.DownstreamScheme) - .WithLoadBalancerOptions(lbOptions) - .WithDownstreamAddresses(downstreamAddresses) - .WithLoadBalancerKey(routeKey) - .WithQosOptions(qosOptions) - .WithRateLimitOptions(rateLimitOption) - .WithHttpHandlerOptions(httpHandlerOptions) - .WithServiceName(fileRoute.ServiceName) - .WithServiceNamespace(fileRoute.ServiceNamespace) - .WithUseServiceDiscovery(fileRouteOptions.UseServiceDiscovery) - .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) - .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) - .WithDelegatingHandlers(fileRoute.DelegatingHandlers) - .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) - .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream) - .WithDangerousAcceptAnyServerCertificateValidator(fileRoute.DangerousAcceptAnyServerCertificateValidator) - .WithSecurityOptions(securityOptions) - .WithDownstreamHttpVersion(downstreamHttpVersion) + + var route = new DownstreamRouteBuilder() + .WithKey(fileRoute.Key) + .WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate) + .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod.ToList()) + .WithUpstreamPathTemplate(upstreamTemplatePattern) + .WithIsAuthenticated(fileRouteOptions.IsAuthenticated) + .WithAuthenticationOptions(authOptionsForRoute) + .WithClaimsToHeaders(claimsToHeaders) + .WithClaimsToClaims(claimsToClaims) + .WithRouteClaimsRequirement(fileRoute.RouteClaimsRequirement) + .WithIsAuthorized(fileRouteOptions.IsAuthorized) + .WithClaimsToQueries(claimsToQueries) + .WithClaimsToDownstreamPath(claimsToDownstreamPath) + .WithRequestIdKey(requestIdKey) + .WithIsCached(fileRouteOptions.IsCached) + .WithCacheOptions(cacheOptions) + .WithDownstreamScheme(fileRoute.DownstreamScheme) + .WithLoadBalancerKey(lbKey) + .WithLoadBalancerOptions(lbOptions) + .WithDownstreamAddresses(downstreamAddresses) + .WithQosOptions(qosOptions) + .WithRateLimitOptions(rateLimitOption) + .WithHttpHandlerOptions(httpHandlerOptions) + .WithServiceName(fileRoute.ServiceName) + .WithServiceNamespace(fileRoute.ServiceNamespace) + .WithUseServiceDiscovery(fileRouteOptions.UseServiceDiscovery) + .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) + .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) + .WithDelegatingHandlers(fileRoute.DelegatingHandlers) + .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) + .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream) + .WithDangerousAcceptAnyServerCertificateValidator(fileRoute.DangerousAcceptAnyServerCertificateValidator) + .WithSecurityOptions(securityOptions) + .WithDownstreamHttpVersion(downstreamHttpVersion) .WithDownstreamHttpVersionPolicy(downstreamHttpVersionPolicy) - .WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod) + .WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod) .WithMetadata(metadata) - .WithTimeout(CreateTimeout(fileRoute, globalConfiguration)) - .Build(); - return route; - } - - private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoute) - { - var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamPathTemplate - var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamHeaders - var upstreamHttpMethods = fileRoute.UpstreamHttpMethod.Count == 0 ? new List() - : fileRoute.UpstreamHttpMethod.Select(x => new HttpMethod(x.Trim())).ToList(); - - return new Route( - [downstreamRoute], - new(), - upstreamHttpMethods, - upstreamTemplatePattern, - fileRoute.UpstreamHost, - aggregator: default, - upstreamHeaderTemplates/*downstreamRoute.UpstreamHeaders*/); - } -} + .WithTimeout(CreateTimeout(fileRoute, globalConfiguration)) + .Build(); + return route; + } + + private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoute) + { + var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamPathTemplate + var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamHeaders + var upstreamHttpMethods = fileRoute.UpstreamHttpMethod.ToHttpMethods(); + return new Route(downstreamRoute) + { + UpstreamHeaderTemplates = upstreamHeaderTemplates, // downstreamRoute.UpstreamHeaders + UpstreamHost = fileRoute.UpstreamHost, + UpstreamHttpMethod = upstreamHttpMethods, + UpstreamTemplatePattern = upstreamTemplatePattern, + }; + } +} diff --git a/src/Ocelot/Configuration/File/FileAggregateRoute.cs b/src/Ocelot/Configuration/File/FileAggregateRoute.cs index a75b900d1..7ca21bd85 100644 --- a/src/Ocelot/Configuration/File/FileAggregateRoute.cs +++ b/src/Ocelot/Configuration/File/FileAggregateRoute.cs @@ -11,7 +11,7 @@ public class FileAggregateRoute : IRouteUpstream, IRouteGroup public List RouteKeysConfig { get; set; } public IDictionary UpstreamHeaderTemplates { get; set; } public string UpstreamHost { get; set; } - public IList UpstreamHttpMethod { get; set; } + public HashSet UpstreamHttpMethod { get; set; } public string UpstreamPathTemplate { get; set; } public FileAggregateRoute() diff --git a/src/Ocelot/Configuration/File/FileDynamicRoute.cs b/src/Ocelot/Configuration/File/FileDynamicRoute.cs index 03cb1f05e..6e90bf491 100644 --- a/src/Ocelot/Configuration/File/FileDynamicRoute.cs +++ b/src/Ocelot/Configuration/File/FileDynamicRoute.cs @@ -1,63 +1,15 @@ -#pragma warning disable IDE0079 // Remove unnecessary suppression -#pragma warning disable SA1133 // Do not combine attributes -#pragma warning disable SA1134 // Attributes should not share line - -using Ocelot.Configuration.Creator; -using System.Text.Json.Serialization; -using NewtonsoftJsonIgnore = Newtonsoft.Json.JsonIgnoreAttribute; - namespace Ocelot.Configuration.File; /// -/// TODO: Make it as a base Route File-model. +/// Represents the JSON structure of a dynamic route in dynamic routing mode using service discovery. /// -public class FileDynamicRoute : IRouteGrouping, IRouteRateLimiting +public class FileDynamicRoute : FileGlobalDynamicRouting, IRouteGrouping, IRouteRateLimiting { - public string DownstreamHttpVersion { get; set; } - - /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. - /// A value of defined constants. - /// - /// Related to the property. - /// - /// HttpVersionPolicy Enum - /// HttpVersion Class - /// HttpRequestMessage.VersionPolicy Property - /// - /// - public string DownstreamHttpVersionPolicy { get; set; } - public IDictionary Metadata { get; set; } + public string Key { get; set; } // IRouteGrouping [Obsolete("Use RateLimitOptions instead of RateLimitRule! Note that RateLimitRule will be removed in version 25.0!")] public FileRateLimitByHeaderRule RateLimitRule { get; set; } - [NewtonsoftJsonIgnore, JsonIgnore] public FileRateLimiting RateLimiting { get; set; } // publish the schema in version 25.0! public string ServiceName { get; set; } - - public FileDynamicRoute() - { - DownstreamHttpVersion = default; - DownstreamHttpVersionPolicy = default; - Metadata = new Dictionary(); - RateLimitRule = default; - RateLimitOptions = default; - RateLimiting = default; - ServiceName = default; - } - - /// The timeout in seconds for requests. - /// A where T is value in seconds. - public int? Timeout { get; set; } - - // IRouteGrouping - public string Key { get; set; } - - // IRouteRateLimiting vs IRouteUpstream - public FileRateLimitByHeaderRule RateLimitOptions { get; set; } // => RateLimitRule; - public IDictionary UpstreamHeaderTemplates => new Dictionary(); - public string UpstreamPathTemplate { get => ServiceName; } - public IList UpstreamHttpMethod { get; set; } - - public bool RouteIsCaseSensitive => false; - public int Priority => 0; + public string ServiceNamespace { get; set; } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index ffe1b5475..6739be799 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -1,14 +1,6 @@ -#pragma warning disable IDE0079 // Remove unnecessary suppression -#pragma warning disable SA1133 // Do not combine attributes -#pragma warning disable SA1134 // Attributes should not share line +namespace Ocelot.Configuration.File; -using Ocelot.Configuration.Creator; -using System.Text.Json.Serialization; -using NewtonsoftJsonIgnore = Newtonsoft.Json.JsonIgnoreAttribute; - -namespace Ocelot.Configuration.File; - -public class FileGlobalConfiguration +public class FileGlobalConfiguration : FileGlobalDynamicRouting { public FileGlobalConfiguration() { @@ -20,7 +12,7 @@ public FileGlobalConfiguration() DownstreamHttpVersionPolicy = default; DownstreamScheme = default; HttpHandlerOptions = new(); - LoadBalancerOptions = new(); + LoadBalancerOptions = default; MetadataOptions = new(); QoSOptions = new(); RateLimitOptions = default; @@ -36,32 +28,13 @@ public FileGlobalConfiguration() public string BaseUrl { get; set; } public FileCacheOptions CacheOptions { get; set; } public IDictionary DownstreamHeaderTransform { get; set; } - public string DownstreamHttpVersion { get; set; } - - /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. - /// A value of defined constants. - /// - /// Related to the property. - /// - /// HttpVersionPolicy Enum - /// HttpVersion Class - /// HttpRequestMessage.VersionPolicy Property - /// - /// - public string DownstreamHttpVersionPolicy { get; set; } - public string DownstreamScheme { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; } - public FileLoadBalancerOptions LoadBalancerOptions { get; set; } public FileMetadataOptions MetadataOptions { get; set; } - public FileQoSOptions QoSOptions { get; set; } - public FileGlobalRateLimitByHeaderRule RateLimitOptions { get; set; } - [NewtonsoftJsonIgnore, JsonIgnore] public FileGlobalRateLimiting RateLimiting { get; set; } // publish the schema in version 25.0! + /*public FileQoSOptions QoSOptions { get; set; }*/ + public new FileGlobalLoadBalancerOptions LoadBalancerOptions { get; set; } + public new FileGlobalRateLimitByHeaderRule RateLimitOptions { get; set; } public string RequestIdKey { get; set; } public FileSecurityOptions SecurityOptions { get; set; } public FileServiceDiscoveryProvider ServiceDiscoveryProvider { get; set; } - - /// The timeout in seconds for requests. - /// A (T is ) value in seconds. - public int? Timeout { get; set; } public IDictionary UpstreamHeaderTransform { get; set; } } diff --git a/src/Ocelot/Configuration/File/FileGlobalDynamicRouting.cs b/src/Ocelot/Configuration/File/FileGlobalDynamicRouting.cs new file mode 100644 index 000000000..a3efd238c --- /dev/null +++ b/src/Ocelot/Configuration/File/FileGlobalDynamicRouting.cs @@ -0,0 +1,42 @@ +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable SA1133 // Do not combine attributes + +using Ocelot.Configuration.Creator; +using System.Text.Json.Serialization; +using NewtonsoftJsonIgnore = Newtonsoft.Json.JsonIgnoreAttribute; + +namespace Ocelot.Configuration.File; + +public class FileGlobalDynamicRouting +{ + /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. + /// A value of defined constants. + /// + /// Related to the property. + /// + /// HttpVersionPolicy Enum + /// HttpVersion Class + /// HttpRequestMessage.VersionPolicy Property + /// + /// + public string DownstreamHttpVersionPolicy { get; set; } + public string DownstreamHttpVersion { get; set; } + public string DownstreamScheme { get; set; } + public FileLoadBalancerOptions LoadBalancerOptions { get; set; } + public IDictionary Metadata { get; set; } + public FileQoSOptions QoSOptions { get; set; } + public FileRateLimitByHeaderRule RateLimitOptions { get; set; } // IRouteRateLimiting + [NewtonsoftJsonIgnore, JsonIgnore] // publish the schema in version 25.0! + public FileRateLimiting RateLimiting { get; set; } + + /// Explicit timeout value which overrides default . + /// Notes: + /// + /// is the consumer of this property. + /// implicitly overrides this property if not defined (null). + /// explicitly overrides this property if QoS is enabled. + /// + /// + /// A (T is ) value, in seconds. + public int? Timeout { get; set; } +} diff --git a/src/Ocelot/Configuration/File/FileGlobalLoadBalancerOptions.cs b/src/Ocelot/Configuration/File/FileGlobalLoadBalancerOptions.cs new file mode 100644 index 000000000..1bb753f2f --- /dev/null +++ b/src/Ocelot/Configuration/File/FileGlobalLoadBalancerOptions.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Configuration.File; + +public class FileGlobalLoadBalancerOptions : FileLoadBalancerOptions, IRouteGroup +{ + public FileGlobalLoadBalancerOptions() : base() { } + public FileGlobalLoadBalancerOptions(string type) : base(type) { } + + /// Gets or sets the keys used to group routes, based on the already defined property. + /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. + /// A (where T is ) collection of keys that determine which routes the options should be applied to. + public HashSet RouteKeys { get; set; } +} diff --git a/src/Ocelot/Configuration/File/FileGlobalRateLimitByHeaderRule.cs b/src/Ocelot/Configuration/File/FileGlobalRateLimitByHeaderRule.cs index 28ba80c86..1f70e4c26 100644 --- a/src/Ocelot/Configuration/File/FileGlobalRateLimitByHeaderRule.cs +++ b/src/Ocelot/Configuration/File/FileGlobalRateLimitByHeaderRule.cs @@ -2,6 +2,11 @@ public class FileGlobalRateLimitByHeaderRule : FileRateLimitByHeaderRule, IRouteGroup { + public FileGlobalRateLimitByHeaderRule() + : base() { } + public FileGlobalRateLimitByHeaderRule(FileRateLimitByHeaderRule from) + : base(from) { } + /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. diff --git a/src/Ocelot/Configuration/File/FileHostAndPort.cs b/src/Ocelot/Configuration/File/FileHostAndPort.cs index 328a8ca12..5de9247dd 100644 --- a/src/Ocelot/Configuration/File/FileHostAndPort.cs +++ b/src/Ocelot/Configuration/File/FileHostAndPort.cs @@ -18,4 +18,6 @@ public FileHostAndPort(string host, int port) public string Host { get; set; } public int Port { get; set; } + + public override string ToString() => $"{Host}:{Port}"; } diff --git a/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs b/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs index 927d5ca59..edf055d54 100644 --- a/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs +++ b/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs @@ -3,10 +3,12 @@ namespace Ocelot.Configuration.File; public class FileLoadBalancerOptions { public FileLoadBalancerOptions() + { } + + public FileLoadBalancerOptions(string type) + : this() { - Expiry = int.MaxValue; - Key = string.Empty; - Type = string.Empty; + Type = type; } public FileLoadBalancerOptions(FileLoadBalancerOptions from) @@ -16,7 +18,7 @@ public FileLoadBalancerOptions(FileLoadBalancerOptions from) Type = from.Type; } - public int Expiry { get; set; } + public int? Expiry { get; set; } public string Key { get; set; } public string Type { get; set; } } diff --git a/src/Ocelot/Configuration/File/FileMetadataOptions.cs b/src/Ocelot/Configuration/File/FileMetadataOptions.cs index 663c700fc..010fa7efa 100644 --- a/src/Ocelot/Configuration/File/FileMetadataOptions.cs +++ b/src/Ocelot/Configuration/File/FileMetadataOptions.cs @@ -7,7 +7,6 @@ public class FileMetadataOptions public FileMetadataOptions() { CurrentCulture = CultureInfo.CurrentCulture.Name; - Metadata = new Dictionary(); NumberStyle = Enum.GetName(NumberStyles.Any); Separators = new[] { "," }; StringSplitOption = Enum.GetName(StringSplitOptions.None); @@ -17,7 +16,6 @@ public FileMetadataOptions() public FileMetadataOptions(FileMetadataOptions from) { CurrentCulture = from.CurrentCulture; - Metadata = from.Metadata; NumberStyle = from.NumberStyle; Separators = from.Separators; StringSplitOption = from.StringSplitOption; @@ -29,5 +27,4 @@ public FileMetadataOptions(FileMetadataOptions from) public string[] Separators { get; set; } public string StringSplitOption { get; set; } public char[] TrimChars { get; set; } - public IDictionary Metadata { get; set; } } diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index 8ae42b866..c96a2b872 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -7,8 +7,11 @@ using NewtonsoftJsonIgnore = Newtonsoft.Json.JsonIgnoreAttribute; namespace Ocelot.Configuration.File; - -public class FileRoute : IRouteGrouping, IRouteRateLimiting, ICloneable // TODO: Inherit from FileDynamicRoute (FileRouteBase) or an interface with FileDynamicRoute props + +/// +/// Represents the JSON structure of a standard static route (no service discovery). +/// +public class FileRoute : IRouteUpstream, IRouteGrouping, IRouteRateLimiting, ICloneable { public FileRoute() { @@ -21,32 +24,32 @@ public FileRoute() DelegatingHandlers = new List(); DownstreamHeaderTransform = new Dictionary(); DownstreamHostAndPorts = new List(); - DownstreamHttpMethod = default; // to be reviewed - DownstreamHttpVersion = default; // to be reviewed - DownstreamHttpVersionPolicy = default; // to be reviewed - DownstreamPathTemplate = default; // to be reviewed + DownstreamHttpMethod = default; + DownstreamHttpVersion = default; + DownstreamHttpVersionPolicy = default; + DownstreamPathTemplate = default; DownstreamScheme = default; // to be reviewed FileCacheOptions = new FileCacheOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); - Key = default; // to be reviewed - LoadBalancerOptions = new FileLoadBalancerOptions(); - Metadata = new Dictionary(); + Key = default; + LoadBalancerOptions = default; + Metadata = default; Priority = 1; // to be reviewed WTF? QoSOptions = new FileQoSOptions(); RateLimiting = default; RateLimitOptions = default; - RequestIdKey = default; // to be reviewed + RequestIdKey = default; RouteClaimsRequirement = new Dictionary(); - RouteIsCaseSensitive = default; // to be reviewed + RouteIsCaseSensitive = default; SecurityOptions = new FileSecurityOptions(); - ServiceName = default; // to be reviewed - ServiceNamespace = default; // to be reviewed - Timeout = default; // to be reviewed + ServiceName = default; + ServiceNamespace = default; + Timeout = default; UpstreamHeaderTemplates = new Dictionary(); UpstreamHeaderTransform = new Dictionary(); - UpstreamHost = default; // to be reviewed - UpstreamHttpMethod = new List(); - UpstreamPathTemplate = default; // to be reviewed + UpstreamHost = default; + UpstreamHttpMethod = new(); + UpstreamPathTemplate = default; } public FileRoute(FileRoute from) @@ -63,8 +66,8 @@ public FileRoute(FileRoute from) public List DelegatingHandlers { get; set; } public IDictionary DownstreamHeaderTransform { get; set; } public List DownstreamHostAndPorts { get; set; } - public string DownstreamHttpMethod { get; set; } - public string DownstreamHttpVersion { get; set; } + public string DownstreamHttpMethod { get; set; } + public string DownstreamHttpVersion { get; set; } /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. /// A value of defined constants. @@ -107,8 +110,8 @@ public FileRoute(FileRoute from) public int? Timeout { get; set; } public IDictionary UpstreamHeaderTemplates { get; set; } public IDictionary UpstreamHeaderTransform { get; set; } - public string UpstreamHost { get; set; } - public IList UpstreamHttpMethod { get; set; } + public string UpstreamHost { get; set; } + public HashSet UpstreamHttpMethod { get; set; } public string UpstreamPathTemplate { get; set; } /// @@ -157,7 +160,7 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.UpstreamHeaderTemplates = new Dictionary(from.UpstreamHeaderTemplates); to.UpstreamHeaderTransform = new Dictionary(from.UpstreamHeaderTransform); to.UpstreamHost = from.UpstreamHost; - to.UpstreamHttpMethod = new List(from.UpstreamHttpMethod); + to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); to.UpstreamPathTemplate = from.UpstreamPathTemplate; } diff --git a/src/Ocelot/Configuration/File/IRouteRateLimiting.cs b/src/Ocelot/Configuration/File/IRouteRateLimiting.cs index 508a92caa..fd52848e7 100644 --- a/src/Ocelot/Configuration/File/IRouteRateLimiting.cs +++ b/src/Ocelot/Configuration/File/IRouteRateLimiting.cs @@ -1,6 +1,6 @@ namespace Ocelot.Configuration.File; -public interface IRouteRateLimiting : IRouteUpstream, IRouteGrouping +public interface IRouteRateLimiting : IRouteGrouping { FileRateLimitByHeaderRule RateLimitOptions { get; } } diff --git a/src/Ocelot/Configuration/File/IRouteUpstream.cs b/src/Ocelot/Configuration/File/IRouteUpstream.cs index 766d46b82..eb84d15ef 100644 --- a/src/Ocelot/Configuration/File/IRouteUpstream.cs +++ b/src/Ocelot/Configuration/File/IRouteUpstream.cs @@ -4,7 +4,7 @@ public interface IRouteUpstream { IDictionary UpstreamHeaderTemplates { get; } string UpstreamPathTemplate { get; } - IList UpstreamHttpMethod { get; } + HashSet UpstreamHttpMethod { get; } bool RouteIsCaseSensitive { get; } int Priority { get; } } diff --git a/src/Ocelot/Configuration/IInternalConfiguration.cs b/src/Ocelot/Configuration/IInternalConfiguration.cs index ad1a2c5fd..d73827071 100644 --- a/src/Ocelot/Configuration/IInternalConfiguration.cs +++ b/src/Ocelot/Configuration/IInternalConfiguration.cs @@ -1,5 +1,3 @@ -using Ocelot.Configuration.File; - namespace Ocelot.Configuration; public interface IInternalConfiguration @@ -21,8 +19,8 @@ public interface IInternalConfiguration HttpHandlerOptions HttpHandlerOptions { get; } Version DownstreamHttpVersion { get; } - - /// Global HTTP version policy. It is related to property. - /// An enumeration value. - HttpVersionPolicy? DownstreamHttpVersionPolicy { get; } + HttpVersionPolicy DownstreamHttpVersionPolicy { get; } + MetadataOptions MetadataOptions { get; } + RateLimitOptions RateLimitOptions { get; } + int? Timeout { get; } } diff --git a/src/Ocelot/Configuration/InternalConfiguration.cs b/src/Ocelot/Configuration/InternalConfiguration.cs index 638d80449..109dec185 100644 --- a/src/Ocelot/Configuration/InternalConfiguration.cs +++ b/src/Ocelot/Configuration/InternalConfiguration.cs @@ -4,6 +4,8 @@ namespace Ocelot.Configuration; public class InternalConfiguration : IInternalConfiguration { + public InternalConfiguration() => Routes = new(); + public InternalConfiguration( List routes, string administrationPath, @@ -14,7 +16,10 @@ public InternalConfiguration( QoSOptions qoSOptions, HttpHandlerOptions httpHandlerOptions, Version downstreamHttpVersion, - HttpVersionPolicy? downstreamHttpVersionPolicy) + HttpVersionPolicy downstreamHttpVersionPolicy, + MetadataOptions metadataOptions, + RateLimitOptions rateLimitOptions, + int? timeout) { Routes = routes; AdministrationPath = administrationPath; @@ -25,21 +30,26 @@ public InternalConfiguration( QoSOptions = qoSOptions; HttpHandlerOptions = httpHandlerOptions; DownstreamHttpVersion = downstreamHttpVersion; - DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; + DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; + MetadataOptions = metadataOptions; + RateLimitOptions = rateLimitOptions; + Timeout = timeout; } - public List Routes { get; } - public string AdministrationPath { get; } - public ServiceProviderConfiguration ServiceProviderConfiguration { get; } - public string RequestId { get; } - public LoadBalancerOptions LoadBalancerOptions { get; } - public string DownstreamScheme { get; } - public QoSOptions QoSOptions { get; } - public HttpHandlerOptions HttpHandlerOptions { get; } - - public Version DownstreamHttpVersion { get; } + public List Routes { get; init; } + public string AdministrationPath { get; init; } + public ServiceProviderConfiguration ServiceProviderConfiguration { get; init; } + public string RequestId { get; init; } + public LoadBalancerOptions LoadBalancerOptions { get; init; } + public string DownstreamScheme { get; init; } + public QoSOptions QoSOptions { get; init; } + public HttpHandlerOptions HttpHandlerOptions { get; init; } /// Global HTTP version policy. It is related to property. /// An enumeration value. - public HttpVersionPolicy? DownstreamHttpVersionPolicy { get; } + public HttpVersionPolicy DownstreamHttpVersionPolicy { get; init; } + public Version DownstreamHttpVersion { get; init; } + public MetadataOptions MetadataOptions { get; init; } + public RateLimitOptions RateLimitOptions { get; init; } + public int? Timeout { get; init; } } diff --git a/src/Ocelot/Configuration/LoadBalancerOptions.cs b/src/Ocelot/Configuration/LoadBalancerOptions.cs index f4906cf73..309c4b38f 100644 --- a/src/Ocelot/Configuration/LoadBalancerOptions.cs +++ b/src/Ocelot/Configuration/LoadBalancerOptions.cs @@ -1,19 +1,33 @@ -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Configuration.File; +using Ocelot.Infrastructure.Extensions; +using Ocelot.LoadBalancer.Balancers; namespace Ocelot.Configuration; public class LoadBalancerOptions { - public LoadBalancerOptions(string type, string key, int expiryInMs) + public LoadBalancerOptions() { - Type = string.IsNullOrWhiteSpace(type) ? nameof(NoLoadBalancer) : type; - Key = key; - ExpiryInMs = expiryInMs; + Type = nameof(NoLoadBalancer); } - public string Type { get; } + public LoadBalancerOptions(FileLoadBalancerOptions options) + : this(options?.Type, options?.Key, options?.Expiry) + { } - public string Key { get; } + public LoadBalancerOptions(string type, string key, int? expiryInMs) + { + Type = type.IfEmpty(nameof(NoLoadBalancer)); + bool isStickySessions = nameof(CookieStickySessions).Equals(type, StringComparison.OrdinalIgnoreCase); + Key = isStickySessions + ? key.IfEmpty(CookieStickySessions.DefSessionCookieName) + : key; + ExpiryInMs = isStickySessions + ? expiryInMs ?? CookieStickySessions.DefSessionExpiryMilliseconds + : expiryInMs ?? 0; + } - public int ExpiryInMs { get; } + public string Type { get; init; } + public string Key { get; init; } + public int ExpiryInMs { get; init; } } diff --git a/src/Ocelot/Configuration/LoadBalancerOptionsBuilder.cs b/src/Ocelot/Configuration/LoadBalancerOptionsBuilder.cs deleted file mode 100644 index 2980fa00e..000000000 --- a/src/Ocelot/Configuration/LoadBalancerOptionsBuilder.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Ocelot.Configuration; - -public class LoadBalancerOptionsBuilder -{ - private string _type; - private string _key; - private int _expiryInMs; - - public LoadBalancerOptionsBuilder WithType(string type) - { - _type = type; - return this; - } - - public LoadBalancerOptionsBuilder WithKey(string key) - { - _key = key; - return this; - } - - public LoadBalancerOptionsBuilder WithExpiryInMs(int expiryInMs) - { - _expiryInMs = expiryInMs; - return this; - } - - public LoadBalancerOptions Build() - { - return new LoadBalancerOptions(_type, _key, _expiryInMs); - } -} diff --git a/src/Ocelot/Configuration/MetadataOptions.cs b/src/Ocelot/Configuration/MetadataOptions.cs index d249d6631..85a4b1de6 100644 --- a/src/Ocelot/Configuration/MetadataOptions.cs +++ b/src/Ocelot/Configuration/MetadataOptions.cs @@ -5,35 +5,45 @@ namespace Ocelot.Configuration; public class MetadataOptions { + public MetadataOptions() + { + CurrentCulture = CultureInfo.CurrentCulture; + NumberStyle = NumberStyles.Any; + Separators = new[] { "," }; + StringSplitOption = StringSplitOptions.None; + TrimChars = new[] { ' ' }; + Metadata = new Dictionary(); + } + public MetadataOptions(MetadataOptions from) { CurrentCulture = from.CurrentCulture; - Metadata = from.Metadata; NumberStyle = from.NumberStyle; Separators = from.Separators; StringSplitOption = from.StringSplitOption; TrimChars = from.TrimChars; + Metadata = from.Metadata; } public MetadataOptions(FileMetadataOptions from) { CurrentCulture = CultureInfo.GetCultureInfo(from.CurrentCulture); - Metadata = from.Metadata; NumberStyle = Enum.Parse(from.NumberStyle); Separators = from.Separators; StringSplitOption = Enum.Parse(from.StringSplitOption); TrimChars = from.TrimChars; + Metadata = new Dictionary(); } public MetadataOptions(string[] separators, char[] trimChars, StringSplitOptions stringSplitOption, NumberStyles numberStyle, CultureInfo currentCulture, IDictionary metadata) { CurrentCulture = currentCulture; - Metadata = metadata; NumberStyle = numberStyle; Separators = separators; StringSplitOption = stringSplitOption; TrimChars = trimChars; + Metadata = metadata; } public CultureInfo CurrentCulture { get; } diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index 5565a2d74..cccb6aadc 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -11,7 +11,6 @@ protected QoSOptions() { } /// The object to copy the properties from. public QoSOptions(QoSOptions from) { - Key = from.Key; DurationOfBreak = from.DurationOfBreak; ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; FailureRatio = from.FailureRatio; @@ -24,7 +23,6 @@ public QoSOptions(QoSOptions from) /// The File-model to copy the properties from. public QoSOptions(FileQoSOptions from) { - Key = string.Empty; DurationOfBreak = from.DurationOfBreak; ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; FailureRatio = from.FailureRatio; @@ -32,8 +30,6 @@ public QoSOptions(FileQoSOptions from) TimeoutValue = from.TimeoutValue; } - public string Key { get; internal set; } - /// Gets the duration, in milliseconds, that the circuit remains open before resetting. /// Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the CircuitBreakerStrategy class. /// A (T is ) value (milliseconds). diff --git a/src/Ocelot/Configuration/Route.cs b/src/Ocelot/Configuration/Route.cs index 515a02fca..ed6f4df0d 100644 --- a/src/Ocelot/Configuration/Route.cs +++ b/src/Ocelot/Configuration/Route.cs @@ -5,28 +5,22 @@ namespace Ocelot.Configuration; public class Route { - public Route(List downstreamRoute, - List downstreamRouteConfig, - IList upstreamHttpMethod, - UpstreamPathTemplate upstreamTemplatePattern, - string upstreamHost, - string aggregator, - IDictionary upstreamHeaderTemplates) + public Route() => DownstreamRoute = new(); + public Route(bool isDynamic) : this() => IsDynamic = isDynamic; + public Route(bool isDynamic, DownstreamRoute route) : this(route) => IsDynamic = isDynamic; + public Route(DownstreamRoute route) => DownstreamRoute = [route]; + public Route(DownstreamRoute route, HttpMethod method) { - UpstreamHost = upstreamHost; - DownstreamRoute = downstreamRoute; - DownstreamRouteConfig = downstreamRouteConfig; - UpstreamHttpMethod = upstreamHttpMethod; - UpstreamTemplatePattern = upstreamTemplatePattern; - Aggregator = aggregator; - UpstreamHeaderTemplates = upstreamHeaderTemplates; + DownstreamRoute = [route]; + UpstreamHttpMethod = [method]; } - public IDictionary UpstreamHeaderTemplates { get; } - public UpstreamPathTemplate UpstreamTemplatePattern { get; } - public IList UpstreamHttpMethod { get; } - public string UpstreamHost { get; } - public List DownstreamRoute { get; } - public List DownstreamRouteConfig { get; } - public string Aggregator { get; } + public bool IsDynamic { get; } + public string Aggregator { get; init; } + public List DownstreamRoute { get; init; } + public List DownstreamRouteConfig { get; init; } + public IDictionary UpstreamHeaderTemplates { get; init; } + public string UpstreamHost { get; init; } + public HashSet UpstreamHttpMethod { get; init; } + public UpstreamPathTemplate UpstreamTemplatePattern { get; init; } } diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs index fea740650..b1123b87c 100644 --- a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs @@ -5,13 +5,13 @@ using Ocelot.Infrastructure; using Ocelot.Responses; using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.Configuration.Validator; /// Validation of a objects. public partial class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator { - private const string Servicefabric = "servicefabric"; private readonly List _serviceDiscoveryFinderDelegates; public FileConfigurationFluentValidator(IServiceProvider provider, RouteFluentValidator routeFluentValidator, FileGlobalConfigurationFluentValidator fileGlobalConfigurationFluentValidator) @@ -62,17 +62,18 @@ public FileConfigurationFluentValidator(IServiceProvider provider, RouteFluentVa .WithMessage((_, aggregateRoute) => $"{nameof(aggregateRoute)} {aggregateRoute.UpstreamPathTemplate} contains Route with specific RequestIdKey, this is not possible with Aggregates"); } + private const string ServiceFabric = ServiceFabricServiceDiscoveryProvider.Type; private bool HaveServiceDiscoveryProviderRegistered(FileRoute route, FileServiceDiscoveryProvider serviceDiscoveryProvider) { return string.IsNullOrEmpty(route.ServiceName) || - serviceDiscoveryProvider?.Type?.ToLower() == Servicefabric || + ServiceFabric.Equals(serviceDiscoveryProvider?.Type, StringComparison.OrdinalIgnoreCase) || _serviceDiscoveryFinderDelegates.Any(); } private bool HaveServiceDiscoveryProviderRegistered(FileServiceDiscoveryProvider serviceDiscoveryProvider) { return serviceDiscoveryProvider == null || - Servicefabric.Equals(serviceDiscoveryProvider.Type, StringComparison.InvariantCultureIgnoreCase) || + ServiceFabric.Equals(serviceDiscoveryProvider.Type, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(serviceDiscoveryProvider.Type) || _serviceDiscoveryFinderDelegates.Any(); } diff --git a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs index 12d40229d..be1e6f912 100644 --- a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Multiplexer; using Ocelot.ServiceDiscovery.Providers; diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 1f857193e..4f297dc0b 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -18,7 +18,9 @@ using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Infrastructure.RequestData; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer; +using Ocelot.LoadBalancer.Creators; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Multiplexer; @@ -57,11 +59,11 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -95,7 +97,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.AddSingleton(); - Services.AddSingleton(); + Services.AddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DiscoveryDownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DiscoveryDownstreamRouteFinder.cs new file mode 100644 index 000000000..cfb798e96 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DiscoveryDownstreamRouteFinder.cs @@ -0,0 +1,127 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.Extensions; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.DownstreamRouteFinder.Finder; + +public class DiscoveryDownstreamRouteFinder : IDownstreamRouteProvider +{ + public const char Dot = '.'; + public const char Slash = '/'; + public const char Question = '?'; + + private readonly ConcurrentDictionary> _cache; + private readonly IRouteKeyCreator _routeKeyCreator; + private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator; + + public DiscoveryDownstreamRouteFinder( + IRouteKeyCreator routeKeyCreator, + IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator) + { + _cache = new(); + _routeKeyCreator = routeKeyCreator; + _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; + } + + public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, + IInternalConfiguration configuration, string upstreamHost, IDictionary upstreamHeaders) + { + var serviceName = GetServiceName(upstreamUrlPath, out var serviceNamespace); + var downstreamPath = GetDownstreamPath(upstreamUrlPath); + var dynamicRoute = configuration.Routes? + .Where(r => r.IsDynamic) // process dynamic routes only + .SelectMany(r => r.DownstreamRoute) + .FirstOrDefault(dr => dr.ServiceName == serviceName && (serviceNamespace.IsEmpty() || dr.ServiceNamespace == serviceNamespace)); + var loadBalancerKey = dynamicRoute != null + ? dynamicRoute.LoadBalancerKey + : _routeKeyCreator.Create(serviceNamespace, serviceName, configuration.LoadBalancerOptions); + if (_cache.TryGetValue(loadBalancerKey, out var downstreamRouteHolder)) + { + return downstreamRouteHolder; + } + + // TODO: Could it be that the static route functionality was possibly lost here? -> StaticRoutesCreator.SetUpRoute -> _upstreamTemplatePatternCreator + var upstreamPathTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue(upstreamUrlPath).Build(); + var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(upstreamHeaders, false); // ? discoveryDownstreamRoute.UpstreamHeaders + + var routeBuilder = new DownstreamRouteBuilder() + .WithUseServiceDiscovery(true) + .WithServiceName(serviceName) + .WithServiceNamespace(serviceNamespace) + .WithDownstreamHttpVersion(configuration.DownstreamHttpVersion) + .WithDownstreamHttpVersionPolicy(configuration.DownstreamHttpVersionPolicy) + .WithDownstreamPathTemplate(downstreamPath) + .WithDownstreamScheme(configuration.DownstreamScheme) + .WithHttpHandlerOptions(configuration.HttpHandlerOptions) + .WithLoadBalancerKey(loadBalancerKey) + .WithLoadBalancerOptions(configuration.LoadBalancerOptions) + .WithMetadata(configuration.MetadataOptions) + .WithQosOptions(configuration.QoSOptions) + .WithRateLimitOptions(configuration.RateLimitOptions) + .WithUpstreamHeaders(upstreamHeaderTemplates as Dictionary) + .WithUpstreamPathTemplate(upstreamPathTemplate) + .WithTimeout(configuration.Timeout); + if (dynamicRoute != null) + { + // We are set to replace IInternalConfiguration global options with the current options from actual dynamic route + routeBuilder + .WithDownstreamHttpVersion(dynamicRoute.DownstreamHttpVersion) + .WithDownstreamHttpVersionPolicy(dynamicRoute.DownstreamHttpVersionPolicy) + .WithDownstreamScheme(dynamicRoute.DownstreamScheme) + .WithLoadBalancerKey(loadBalancerKey/*dynamicRoute.LoadBalancerKey*/) + .WithLoadBalancerOptions(dynamicRoute.LoadBalancerOptions) + .WithMetadata(dynamicRoute.MetadataOptions) + .WithQosOptions(dynamicRoute.QosOptions) + .WithRateLimitOptions(dynamicRoute.RateLimitOptions) + .WithServiceName(serviceName/*dynamicRoute.ServiceName*/) + .WithServiceNamespace(serviceNamespace/*dynamicRoute.ServiceNamespace*/) + .WithTimeout(dynamicRoute.Timeout); + } + + var downstreamRoute = routeBuilder.Build(); + var route = new Route(true, downstreamRoute) // IsDynamic -> true + { + UpstreamHeaderTemplates = upstreamHeaderTemplates, + UpstreamHost = upstreamHost, + UpstreamHttpMethod = [new(upstreamHttpMethod.Trim())], + UpstreamTemplatePattern = upstreamPathTemplate, + }; + downstreamRouteHolder = new OkResponse(new DownstreamRouteHolder(new List(), route)); + _cache.AddOrUpdate(loadBalancerKey, downstreamRouteHolder, (x, y) => downstreamRouteHolder); + return downstreamRouteHolder; + } + + private static string GetDownstreamPath(string upstreamUrlPath) + { + int index = upstreamUrlPath.IndexOf(Slash, 1); + return index != -1 + ? upstreamUrlPath[index..] + : Slash.ToString(); + } + + /// Gets service name and its namespace of request URL. + /// Note: A namespace and service name should be separated by a '.' (dot) character. + /// Example: http://ocelot.net/namespace.service-name/path URL. + /// The upstream path. + /// Extracted namespace. + /// A object. + protected virtual string GetServiceName(string upstreamUrlPath, out string serviceNamespace) + { + var path = upstreamUrlPath.AsSpan(); + int index = path[1..].IndexOf(Slash); + var name = index == -1 + ? path[1..] + : path.Slice(1, index).TrimEnd(Slash); + + index = name.IndexOf(Dot); + serviceNamespace = index == -1 + ? string.Empty + : name[..index].ToString(); + var serviceName = index == -1 ? name : name[++index..]; + return serviceName.ToString(); + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs deleted file mode 100644 index fdb64d620..000000000 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.Creator; -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Responses; -using Ocelot.Values; - -namespace Ocelot.DownstreamRouteFinder.Finder; - -/// -/// TODO: Rename to ServiceDiscoveryDownstreamRouteCreator or ServiceDiscoveryRoutesCreator or MultiplexingRoutesCreator (see DownstreamRouteHolder refs). -/// -public class DownstreamRouteCreator : IDownstreamRouteProvider -{ - private readonly ConcurrentDictionary> _cache; - private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator; - - public DownstreamRouteCreator(IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator) - { - _cache = new(); - _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; - } - - public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, - IInternalConfiguration configuration, string upstreamHost, IDictionary upstreamHeaders) - { - var serviceName = GetServiceName(upstreamUrlPath); - var downstreamPath = GetDownstreamPath(upstreamUrlPath); - if (HasQueryString(downstreamPath)) - { - downstreamPath = RemoveQueryString(downstreamPath); - } - - var downstreamPathForKeys = $"/{serviceName}{downstreamPath}"; - var loadBalancerKey = CreateLoadBalancerKey(downstreamPathForKeys, upstreamHttpMethod, configuration.LoadBalancerOptions); - if (_cache.TryGetValue(loadBalancerKey, out var downstreamRouteHolder)) - { - return downstreamRouteHolder; - } - - var qosOptions = new QoSOptions(configuration.QoSOptions) - { - Key = $"{downstreamPathForKeys}|{upstreamHttpMethod}", - }; - - // TODO: Could it be that the static route functionality was possibly lost here? -> RoutesCreator.SetUpRoute -> _upstreamTemplatePatternCreator - var upstreamPathTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue(upstreamUrlPath).Build(); - var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(upstreamHeaders, false); // ? serviceDiscoveryDownstreamRoute.UpstreamHeaders - - var downstreamRouteBuilder = new DownstreamRouteBuilder() - .WithServiceName(serviceName) - .WithLoadBalancerKey(loadBalancerKey) - .WithDownstreamPathTemplate(downstreamPath) - .WithUseServiceDiscovery(true) - .WithHttpHandlerOptions(configuration.HttpHandlerOptions) - .WithQosOptions(qosOptions) - .WithDownstreamScheme(configuration.DownstreamScheme) - .WithLoadBalancerOptions(configuration.LoadBalancerOptions) - .WithDownstreamHttpVersion(configuration.DownstreamHttpVersion) - .WithUpstreamPathTemplate(upstreamPathTemplate) - .WithUpstreamHeaders(upstreamHeaderTemplates as Dictionary); - - // TODO: Review this logic. Is this merging options for dynamic routes? - var serviceDiscoveryDownstreamRoute = configuration.Routes? - .SelectMany(x => x.DownstreamRoute) - .FirstOrDefault(x => x.ServiceName == serviceName); - if (serviceDiscoveryDownstreamRoute != null) - { - downstreamRouteBuilder - .WithRateLimitOptions(serviceDiscoveryDownstreamRoute.RateLimitOptions); - } - - var downstreamRoute = downstreamRouteBuilder.Build(); - var route = new Route( - new() { downstreamRoute }, - new(), - new List() { new(upstreamHttpMethod.Trim()) }, - upstreamPathTemplate, - upstreamHost, - aggregator: default, - upstreamHeaderTemplates); - - downstreamRouteHolder = new OkResponse(new DownstreamRouteHolder(new List(), route)); - _cache.AddOrUpdate(loadBalancerKey, downstreamRouteHolder, (x, y) => downstreamRouteHolder); - - return downstreamRouteHolder; - } - - private static string RemoveQueryString(string downstreamPath) - { - return downstreamPath - .Substring(0, downstreamPath.IndexOf('?')); - } - - private static bool HasQueryString(string downstreamPath) - { - return downstreamPath.Contains('?'); - } - - private static string GetDownstreamPath(string upstreamUrlPath) - { - if (upstreamUrlPath.IndexOf('/', 1) == -1) - { - return "/"; - } - - return upstreamUrlPath - .Substring(upstreamUrlPath.IndexOf('/', 1)); - } - - private static string GetServiceName(string upstreamUrlPath) - { - if (upstreamUrlPath.IndexOf('/', 1) == -1) - { - return upstreamUrlPath - .Substring(1); - } - - return upstreamUrlPath - .Substring(1, upstreamUrlPath.IndexOf('/', 1)) - .TrimEnd('/'); - } - - private static string CreateLoadBalancerKey(string downstreamTemplatePath, string httpMethod, LoadBalancerOptions loadBalancerOptions) - { - if (!string.IsNullOrEmpty(loadBalancerOptions.Type) && !string.IsNullOrEmpty(loadBalancerOptions.Key) && loadBalancerOptions.Type == nameof(CookieStickySessions)) - { - return $"{nameof(CookieStickySessions)}:{loadBalancerOptions.Key}"; - } - - return CreateQoSKey(downstreamTemplatePath, httpMethod); - } - - private static string CreateQoSKey(string downstreamTemplatePath, string httpMethod) - { - var loadBalancerKey = $"{downstreamTemplatePath}|{httpMethod}"; - return loadBalancerKey; - } -} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 2dff92dde..24c1d4169 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -30,7 +30,7 @@ public Response Get(string upstreamUrlPath, string upstre var downstreamRoutes = new List(); var applicableRoutes = configuration.Routes - .Where(r => RouteIsApplicableToThisRequest(r, httpMethod, upstreamHost)) + .Where(r => !r.IsDynamic && RouteIsApplicableToThisRequest(r, httpMethod, upstreamHost)) // process static routes only .OrderByDescending(x => x.UpstreamTemplatePattern.Priority); foreach (var route in applicableRoutes) @@ -55,7 +55,9 @@ public Response Get(string upstreamUrlPath, string upstre private static bool RouteIsApplicableToThisRequest(Route route, string httpMethod, string upstreamHost) { - return (route.UpstreamHttpMethod.Count == 0 || route.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(httpMethod.ToLower())) && + var method = new HttpMethod(httpMethod.Trim()); + return (route.UpstreamHttpMethod.Count == 0 || route.UpstreamHttpMethod.Contains(method)) + && (string.IsNullOrEmpty(route.UpstreamHost) || route.UpstreamHost == upstreamHost); } diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs index ed4af167d..e1b6c5e58 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs @@ -6,7 +6,7 @@ namespace Ocelot.DownstreamRouteFinder.Finder; public class DownstreamRouteProviderFactory : IDownstreamRouteProviderFactory { - private readonly Dictionary _providers; + private readonly Dictionary _providers; // TODO We need to use a HashSet here for quicker lookups private readonly IOcelotLogger _logger; public DownstreamRouteProviderFactory(IServiceProvider provider, IOcelotLoggerFactory factory) @@ -19,11 +19,11 @@ public IDownstreamRouteProvider Get(IInternalConfiguration config) { //todo - this is a bit hacky we are saying there are no routes or there are routes but none of them have //an upstream path template which means they are dyanmic and service discovery is on... - if ((!config.Routes.Any() || config.Routes.All(x => string.IsNullOrEmpty(x.UpstreamTemplatePattern?.OriginalValue))) && IsServiceDiscovery(config.ServiceProviderConfiguration)) + if ((config.Routes.Count == 0 || config.Routes.All(x => string.IsNullOrEmpty(x.UpstreamTemplatePattern?.OriginalValue))) && IsServiceDiscovery(config.ServiceProviderConfiguration)) { - _logger.LogInformation($"Selected {nameof(DownstreamRouteCreator)} as DownstreamRouteProvider for this request"); + _logger.LogInformation($"Selected {nameof(DiscoveryDownstreamRouteFinder)} as {nameof(IDownstreamRouteProvider)} for this request"); - return _providers[nameof(DownstreamRouteCreator)]; + return _providers[nameof(DiscoveryDownstreamRouteFinder)]; } return _providers[nameof(DownstreamRouteFinder)]; @@ -31,6 +31,6 @@ public IDownstreamRouteProvider Get(IInternalConfiguration config) private static bool IsServiceDiscovery(ServiceProviderConfiguration config) { - return !string.IsNullOrEmpty(config?.Host) && config?.Port > 0 && !string.IsNullOrEmpty(config.Type); + return !string.IsNullOrEmpty(config?.Host) && config?.Port > 0 && !string.IsNullOrEmpty(config?.Type); } } diff --git a/src/Ocelot/Infrastructure/Extensions/IEnumerableExtensions.cs b/src/Ocelot/Infrastructure/Extensions/IEnumerableExtensions.cs new file mode 100644 index 000000000..de3068708 --- /dev/null +++ b/src/Ocelot/Infrastructure/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,28 @@ +namespace Ocelot.Infrastructure.Extensions; + +public static class IEnumerableExtensions +{ + /// + /// Converts a collection of representations of HTTP methods (verbs) into a hashed set of objects. + /// + /// Note: + /// + /// Trims each string in the collection. + /// Does not throw if the collection is . + /// + /// + /// The collection of HTTP method strings. + /// A object, where T is . + public static HashSet ToHttpMethods(this IEnumerable collection) + { + collection ??= Enumerable.Empty(); + return collection.Select(verb => new HttpMethod(verb.Trim())).ToHashSet(); + } + + /// + /// Helper function to convert multiple strings into a comma-separated string aka CSV. + /// + /// The collection of strings to join by comma separator. + /// A in the comma-separated format. + public static string Csv(this IEnumerable values) => string.Join(',', values); +} diff --git a/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs index de2f78527..d8b5fbabb 100644 --- a/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs +++ b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs @@ -2,13 +2,13 @@ public static class StringExtensions { - /// Indicates whether a specified string is null, empty, or consists only of white-space characters. + /// Indicates whether a specified string is , empty, or consists only of white-space characters. /// This is shortcut for the method. /// The string to test. /// if the parameter is or , or if consists exclusively of white-space characters. - public static bool IsNullOrEmpty(this string str) => string.IsNullOrWhiteSpace(str); + public static bool IsEmpty(this string str) => string.IsNullOrWhiteSpace(str); - /// Defaults to the default string if the current string is empty. + /// Defaults to the default string if the current string is null or empty. /// Based on the method. /// The current string. /// The default string. diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs b/src/Ocelot/LoadBalancer/Balancers/CookieStickySessions.cs similarity index 68% rename from src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs rename to src/Ocelot/LoadBalancer/Balancers/CookieStickySessions.cs index 5499c9b19..3e19e33f8 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs +++ b/src/Ocelot/LoadBalancer/Balancers/CookieStickySessions.cs @@ -1,19 +1,45 @@ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.LoadBalancer.LoadBalancers; +namespace Ocelot.LoadBalancer.Balancers; public class CookieStickySessions : ILoadBalancer { + /// + /// Track default ASP.NET Core session idle timeout here: SessionOptions.IdleTimeout. + /// + public const int DefSessionExpiryMinutes = 20; + public static readonly int DefSessionExpiryMilliseconds; + + /// + /// Track default ASP.NET Core session cookie name here: SessionDefaults.CookieName. + /// + public static readonly string DefSessionCookieName = Microsoft.AspNetCore.Session.SessionDefaults.CookieName; + + static CookieStickySessions() + { +#if NET9_0_OR_GREATER + DefSessionExpiryMilliseconds = DefSessionExpiryMinutes * (int)TimeSpan.MillisecondsPerMinute; +#else + // TODO Migrate to TimeSpan.MillisecondsPerMinute after net8.0 deprecation + DefSessionExpiryMilliseconds = (int)TimeSpan.FromMinutes(DefSessionExpiryMinutes).TotalMilliseconds; +#endif + } + private readonly int _keyExpiryInMs; private readonly string _cookieName; private readonly ILoadBalancer _loadBalancer; private readonly IBus _bus; +#if NET9_0_OR_GREATER + private static readonly Lock Locker = new(); +#else private static readonly object Locker = new(); +#endif private static readonly Dictionary Stored = new(); // TODO Inject instead of static sharing public string Type => nameof(CookieStickySessions); @@ -50,7 +76,7 @@ public Task> LeaseAsync(HttpContext httpContext) var key = $"{serviceName}:{cookie}"; // strong key name because of static store lock (Locker) { - if (!string.IsNullOrEmpty(key) && Stored.TryGetValue(key, out StickySession cached)) + if (Stored.TryGetValue(key, out StickySession cached)) { var updated = new StickySession(cached.HostAndPort, DateTime.UtcNow.AddMilliseconds(_keyExpiryInMs), key); Update(key, updated); diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs b/src/Ocelot/LoadBalancer/Balancers/LeastConnection.cs similarity index 93% rename from src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs rename to src/Ocelot/LoadBalancer/Balancers/LeastConnection.cs index 21d19b11b..778ba5f9f 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs +++ b/src/Ocelot/LoadBalancer/Balancers/LeastConnection.cs @@ -1,15 +1,21 @@ using Microsoft.AspNetCore.Http; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.LoadBalancer.LoadBalancers; +namespace Ocelot.LoadBalancer.Balancers; public class LeastConnection : ILoadBalancer { private readonly Func>> _services; private readonly List _leases; private readonly string _serviceName; +#if NET9_0_OR_GREATER + private static readonly Lock SyncRoot = new(); +#else private static readonly object SyncRoot = new(); +#endif public string Type => nameof(LeastConnection); diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs b/src/Ocelot/LoadBalancer/Balancers/NoLoadBalancer.cs similarity index 88% rename from src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs rename to src/Ocelot/LoadBalancer/Balancers/NoLoadBalancer.cs index fb3a035ee..41e553ece 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs +++ b/src/Ocelot/LoadBalancer/Balancers/NoLoadBalancer.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Http; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.LoadBalancer.LoadBalancers; +namespace Ocelot.LoadBalancer.Balancers; public class NoLoadBalancer : ILoadBalancer { diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs b/src/Ocelot/LoadBalancer/Balancers/RoundRobin.cs similarity index 92% rename from src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs rename to src/Ocelot/LoadBalancer/Balancers/RoundRobin.cs index 8febb5b29..f11c1eb4f 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs +++ b/src/Ocelot/LoadBalancer/Balancers/RoundRobin.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Http; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.LoadBalancer.LoadBalancers; +namespace Ocelot.LoadBalancer.Balancers; public class RoundRobin : ILoadBalancer { @@ -14,20 +16,25 @@ public class RoundRobin : ILoadBalancer public RoundRobin(Func>> services, string serviceName) { + ArgumentNullException.ThrowIfNull(services); _servicesDelegate = services; _serviceName = serviceName; _leasing = new(); } private static readonly Dictionary LastIndices = new(); - protected static readonly object SyncRoot = new(); +#if NET9_0_OR_GREATER + private static readonly Lock SyncRoot = new(); +#else + private static readonly object SyncRoot = new(); +#endif public event EventHandler Leased; protected virtual void OnLeased(LeaseEventArgs e) => Leased?.Invoke(this, e); public virtual async Task> LeaseAsync(HttpContext httpContext) { - var services = await _servicesDelegate?.Invoke() ?? new List(); + var services = await _servicesDelegate.Invoke() ?? new List(); if (services.Count == 0) { return new ErrorResponse(new ServicesAreEmptyError($"There were no services in {Type} for '{_serviceName}' during {nameof(LeaseAsync)} operation!")); diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessionsCreator.cs b/src/Ocelot/LoadBalancer/Creators/CookieStickySessionsCreator.cs similarity index 85% rename from src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessionsCreator.cs rename to src/Ocelot/LoadBalancer/Creators/CookieStickySessionsCreator.cs index 62e031c08..eca206723 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessionsCreator.cs +++ b/src/Ocelot/LoadBalancer/Creators/CookieStickySessionsCreator.cs @@ -1,20 +1,22 @@ using Ocelot.Configuration; using Ocelot.Infrastructure; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; -using Ocelot.ServiceDiscovery.Providers; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class CookieStickySessionsCreator : ILoadBalancerCreator -{ - public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) - { - var options = route.LoadBalancerOptions; +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.Creators; + +public class CookieStickySessionsCreator : ILoadBalancerCreator +{ + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + var options = route.LoadBalancerOptions; var loadBalancer = new RoundRobin(serviceProvider.GetAsync, route.LoadBalancerKey); - var bus = new InMemoryBus(); - return new OkResponse( - new CookieStickySessions(loadBalancer, options.Key, options.ExpiryInMs, bus)); - } - - public string Type => nameof(CookieStickySessions); -} + var bus = new InMemoryBus(); + return new OkResponse( + new CookieStickySessions(loadBalancer, options.Key, options.ExpiryInMs, bus)); + } + + public string Type => nameof(CookieStickySessions); +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/DelegateInvokingLoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/Creators/DelegateInvokingLoadBalancerCreator.cs similarity index 79% rename from src/Ocelot/LoadBalancer/LoadBalancers/DelegateInvokingLoadBalancerCreator.cs rename to src/Ocelot/LoadBalancer/Creators/DelegateInvokingLoadBalancerCreator.cs index f9b9156a8..a1ed569ba 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/DelegateInvokingLoadBalancerCreator.cs +++ b/src/Ocelot/LoadBalancer/Creators/DelegateInvokingLoadBalancerCreator.cs @@ -1,31 +1,33 @@ using Ocelot.Configuration; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; -using Ocelot.ServiceDiscovery.Providers; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class DelegateInvokingLoadBalancerCreator : ILoadBalancerCreator - where T : ILoadBalancer -{ - private readonly Func _creatorFunc; - - public DelegateInvokingLoadBalancerCreator( - Func creatorFunc) - { - _creatorFunc = creatorFunc; - } - - public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) - { - try - { - return new OkResponse(_creatorFunc(route, serviceProvider)); - } - catch (Exception e) - { - return new ErrorResponse(new ErrorInvokingLoadBalancerCreator(e)); - } - } - - public string Type => typeof(T).Name; -} +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.Creators; + +public class DelegateInvokingLoadBalancerCreator : ILoadBalancerCreator + where T : ILoadBalancer +{ + private readonly Func _creatorFunc; + + public DelegateInvokingLoadBalancerCreator( + Func creatorFunc) + { + _creatorFunc = creatorFunc; + } + + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + try + { + return new OkResponse(_creatorFunc(route, serviceProvider)); + } + catch (Exception e) + { + return new ErrorResponse(new InvokingLoadBalancerCreatorError(e)); + } + } + + public string Type => typeof(T).Name; +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionCreator.cs b/src/Ocelot/LoadBalancer/Creators/LeastConnectionCreator.cs similarity index 85% rename from src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionCreator.cs rename to src/Ocelot/LoadBalancer/Creators/LeastConnectionCreator.cs index e5e139a3b..6421cb7b6 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionCreator.cs +++ b/src/Ocelot/LoadBalancer/Creators/LeastConnectionCreator.cs @@ -1,20 +1,22 @@ using Ocelot.Configuration; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; -using Ocelot.ServiceDiscovery.Providers; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class LeastConnectionCreator : ILoadBalancerCreator -{ - public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) - { - var loadBalancer = new LeastConnection( - serviceProvider.GetAsync, - !string.IsNullOrEmpty(route.ServiceName) - ? route.ServiceName - : route.LoadBalancerKey); // if service discovery mode then use service name; otherwise use balancer key +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.Creators; + +public class LeastConnectionCreator : ILoadBalancerCreator +{ + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + var loadBalancer = new LeastConnection( + serviceProvider.GetAsync, + !string.IsNullOrEmpty(route.ServiceName) + ? route.ServiceName + : route.LoadBalancerKey); // if service discovery mode then use service name; otherwise use balancer key return new OkResponse(loadBalancer); - } - - public string Type => nameof(LeastConnection); -} + } + + public string Type => nameof(LeastConnection); +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/Creators/NoLoadBalancerCreator.cs similarity index 79% rename from src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancerCreator.cs rename to src/Ocelot/LoadBalancer/Creators/NoLoadBalancerCreator.cs index 2f2495c61..f74207f32 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancerCreator.cs +++ b/src/Ocelot/LoadBalancer/Creators/NoLoadBalancerCreator.cs @@ -1,15 +1,17 @@ using Ocelot.Configuration; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; -using Ocelot.ServiceDiscovery.Providers; - -namespace Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.ServiceDiscovery.Providers; -public class NoLoadBalancerCreator : ILoadBalancerCreator -{ - public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) - { +namespace Ocelot.LoadBalancer.Creators; + +public class NoLoadBalancerCreator : ILoadBalancerCreator +{ + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { return new OkResponse(new NoLoadBalancer(async () => await serviceProvider.GetAsync())); - } - - public string Type => nameof(NoLoadBalancer); -} + } + + public string Type => nameof(NoLoadBalancer); +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinCreator.cs b/src/Ocelot/LoadBalancer/Creators/RoundRobinCreator.cs similarity index 85% rename from src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinCreator.cs rename to src/Ocelot/LoadBalancer/Creators/RoundRobinCreator.cs index c45720e28..f8f93b462 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinCreator.cs +++ b/src/Ocelot/LoadBalancer/Creators/RoundRobinCreator.cs @@ -1,20 +1,22 @@ using Ocelot.Configuration; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; -using Ocelot.ServiceDiscovery.Providers; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class RoundRobinCreator : ILoadBalancerCreator -{ - public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) - { - var loadBalancer = new RoundRobin( - serviceProvider.GetAsync, - !string.IsNullOrEmpty(route.ServiceName) - ? route.ServiceName - : route.LoadBalancerKey); // if service discovery mode then use service name; otherwise use balancer key +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.Creators; + +public class RoundRobinCreator : ILoadBalancerCreator +{ + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + var loadBalancer = new RoundRobin( + serviceProvider.GetAsync, + !string.IsNullOrEmpty(route.ServiceName) + ? route.ServiceName + : route.LoadBalancerKey); // if service discovery mode then use service name; otherwise use balancer key return new OkResponse(loadBalancer); - } - - public string Type => nameof(RoundRobin); -} + } + + public string Type => nameof(RoundRobin); +} diff --git a/src/Ocelot/LoadBalancer/Errors/CouldNotFindLoadBalancerCreatorError.cs b/src/Ocelot/LoadBalancer/Errors/CouldNotFindLoadBalancerCreatorError.cs new file mode 100644 index 000000000..8f9887104 --- /dev/null +++ b/src/Ocelot/LoadBalancer/Errors/CouldNotFindLoadBalancerCreatorError.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.Errors; + +public class CouldNotFindLoadBalancerCreatorError : Error +{ + public CouldNotFindLoadBalancerCreatorError(string message) + : base(message, OcelotErrorCode.CouldNotFindLoadBalancerCreator, StatusCodes.Status404NotFound) + { + } +} diff --git a/src/Ocelot/LoadBalancer/Errors/InvokingLoadBalancerCreatorError.cs b/src/Ocelot/LoadBalancer/Errors/InvokingLoadBalancerCreatorError.cs new file mode 100644 index 000000000..7fd263745 --- /dev/null +++ b/src/Ocelot/LoadBalancer/Errors/InvokingLoadBalancerCreatorError.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.Errors; + +public class InvokingLoadBalancerCreatorError : Error +{ + public InvokingLoadBalancerCreatorError(Exception e) + : base($"Error when invoking user provided load balancer creator function, Message: {e.Message}, StackTrace: {e.StackTrace}", + OcelotErrorCode.ErrorInvokingLoadBalancerCreator, + StatusCodes.Status500InternalServerError) + { + } +} diff --git a/src/Ocelot/LoadBalancer/Errors/ServicesAreEmptyError.cs b/src/Ocelot/LoadBalancer/Errors/ServicesAreEmptyError.cs new file mode 100644 index 000000000..f189fe610 --- /dev/null +++ b/src/Ocelot/LoadBalancer/Errors/ServicesAreEmptyError.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.Errors; + +public class ServicesAreEmptyError : Error +{ + public ServicesAreEmptyError(string message) + : base(message, OcelotErrorCode.ServicesAreEmptyError, StatusCodes.Status404NotFound) + { + } +} diff --git a/src/Ocelot/LoadBalancer/Errors/ServicesAreNullError.cs b/src/Ocelot/LoadBalancer/Errors/ServicesAreNullError.cs new file mode 100644 index 000000000..8822b0f04 --- /dev/null +++ b/src/Ocelot/LoadBalancer/Errors/ServicesAreNullError.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.Errors; + +public class ServicesAreNullError : Error +{ + public ServicesAreNullError(string message) + : base(message, OcelotErrorCode.ServicesAreNullError, StatusCodes.Status404NotFound) + { + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs b/src/Ocelot/LoadBalancer/Errors/UnableToFindLoadBalancerError.cs similarity index 65% rename from src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs rename to src/Ocelot/LoadBalancer/Errors/UnableToFindLoadBalancerError.cs index 8fcc99061..603922590 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs +++ b/src/Ocelot/LoadBalancer/Errors/UnableToFindLoadBalancerError.cs @@ -1,11 +1,12 @@ +using Microsoft.AspNetCore.Http; using Ocelot.Errors; -namespace Ocelot.LoadBalancer.LoadBalancers; +namespace Ocelot.LoadBalancer.Errors; public class UnableToFindLoadBalancerError : Error { public UnableToFindLoadBalancerError(string message) - : base(message, OcelotErrorCode.UnableToFindLoadBalancerError, 404) + : base(message, OcelotErrorCode.UnableToFindLoadBalancerError, StatusCodes.Status404NotFound) { } } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs b/src/Ocelot/LoadBalancer/Interfaces/ILoadBalancer.cs similarity index 93% rename from src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs rename to src/Ocelot/LoadBalancer/Interfaces/ILoadBalancer.cs index 9e1ebfd45..50a9d9224 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs +++ b/src/Ocelot/LoadBalancer/Interfaces/ILoadBalancer.cs @@ -3,7 +3,7 @@ using Ocelot.Values; using System.Reflection; -namespace Ocelot.LoadBalancer.LoadBalancers; +namespace Ocelot.LoadBalancer.Interfaces; // TODO Add sync & async pairs public interface ILoadBalancer diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerCreator.cs similarity index 83% rename from src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerCreator.cs rename to src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerCreator.cs index 8c728db6e..781d0433c 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerCreator.cs +++ b/src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerCreator.cs @@ -1,11 +1,11 @@ using Ocelot.Configuration; using Ocelot.Responses; -using Ocelot.ServiceDiscovery.Providers; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public interface ILoadBalancerCreator -{ - Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider); - string Type { get; } -} +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.Interfaces; + +public interface ILoadBalancerCreator +{ + Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider); + string Type { get; } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerFactory.cs similarity index 78% rename from src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs rename to src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerFactory.cs index 8c891ef14..f0aafd0bf 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs +++ b/src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerFactory.cs @@ -1,9 +1,9 @@ using Ocelot.Configuration; -using Ocelot.Responses; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public interface ILoadBalancerFactory -{ - Response Get(DownstreamRoute route, ServiceProviderConfiguration config); -} +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.Interfaces; + +public interface ILoadBalancerFactory +{ + Response Get(DownstreamRoute route, ServiceProviderConfiguration config); +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerHouse.cs similarity index 77% rename from src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs rename to src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerHouse.cs index 17dacdde2..542874313 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerHouse.cs @@ -1,9 +1,9 @@ -using Ocelot.Configuration; -using Ocelot.Responses; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public interface ILoadBalancerHouse -{ - Response Get(DownstreamRoute route, ServiceProviderConfiguration config); -} +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.Interfaces; + +public interface ILoadBalancerHouse +{ + Response Get(DownstreamRoute route, ServiceProviderConfiguration config); +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancerFactory.cs similarity index 82% rename from src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs rename to src/Ocelot/LoadBalancer/LoadBalancerFactory.cs index 39066fd4a..03d74e66f 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancerFactory.cs @@ -1,45 +1,48 @@ using Ocelot.Configuration; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; -using Ocelot.ServiceDiscovery; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class LoadBalancerFactory : ILoadBalancerFactory -{ - private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory; - private readonly IEnumerable _loadBalancerCreators; - - public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory, IEnumerable loadBalancerCreators) - { - _serviceProviderFactory = serviceProviderFactory; - _loadBalancerCreators = loadBalancerCreators; - } - - public Response Get(DownstreamRoute route, ServiceProviderConfiguration config) - { - var serviceProviderFactoryResponse = _serviceProviderFactory.Get(config, route); - - if (serviceProviderFactoryResponse.IsError) - { - return new ErrorResponse(serviceProviderFactoryResponse.Errors); - } - - var serviceProvider = serviceProviderFactoryResponse.Data; - var requestedType = route.LoadBalancerOptions?.Type ?? nameof(NoLoadBalancer); - var applicableCreator = _loadBalancerCreators.SingleOrDefault(c => c.Type == requestedType); - - if (applicableCreator == null) - { - return new ErrorResponse(new CouldNotFindLoadBalancerCreator($"Could not find load balancer creator for Type: {requestedType}, please check your config specified the correct load balancer and that you have registered a class with the same name.")); - } - - var createdLoadBalancerResponse = applicableCreator.Create(route, serviceProvider); - - if (createdLoadBalancerResponse.IsError) - { - return new ErrorResponse(createdLoadBalancerResponse.Errors); - } - - return new OkResponse(createdLoadBalancerResponse.Data); - } -} +using Ocelot.ServiceDiscovery; + +namespace Ocelot.LoadBalancer; + +public class LoadBalancerFactory : ILoadBalancerFactory +{ + private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory; + private readonly IEnumerable _loadBalancerCreators; + + public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory, IEnumerable loadBalancerCreators) + { + _serviceProviderFactory = serviceProviderFactory; + _loadBalancerCreators = loadBalancerCreators; + } + + public Response Get(DownstreamRoute route, ServiceProviderConfiguration config) + { + var serviceProviderFactoryResponse = _serviceProviderFactory.Get(config, route); + + if (serviceProviderFactoryResponse.IsError) + { + return new ErrorResponse(serviceProviderFactoryResponse.Errors); + } + + var serviceProvider = serviceProviderFactoryResponse.Data; + var requestedType = route.LoadBalancerOptions?.Type ?? nameof(NoLoadBalancer); + var applicableCreator = _loadBalancerCreators.SingleOrDefault(c => c.Type == requestedType); + + if (applicableCreator == null) + { + return new ErrorResponse(new CouldNotFindLoadBalancerCreatorError($"Could not find load balancer creator for Type: {requestedType}, please check your config specified the correct load balancer and that you have registered a class with the same name.")); + } + + var createdLoadBalancerResponse = applicableCreator.Create(route, serviceProvider); + + if (createdLoadBalancerResponse.IsError) + { + return new ErrorResponse(createdLoadBalancerResponse.Errors); + } + + return new OkResponse(createdLoadBalancerResponse.Data); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancerHouse.cs similarity index 77% rename from src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs rename to src/Ocelot/LoadBalancer/LoadBalancerHouse.cs index 10685ce41..8479a4883 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancerHouse.cs @@ -1,37 +1,43 @@ -using Ocelot.Configuration; -using Ocelot.Responses; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class LoadBalancerHouse : ILoadBalancerHouse -{ - private readonly ILoadBalancerFactory _factory; - private readonly Dictionary _loadBalancers; - private static readonly object SyncRoot = new(); - - public LoadBalancerHouse(ILoadBalancerFactory factory) - { - _factory = factory; - _loadBalancers = new(); - } - - public Response Get(DownstreamRoute route, ServiceProviderConfiguration config) - { - try - { - lock (SyncRoot) - { - return (_loadBalancers.TryGetValue(route.LoadBalancerKey, out var loadBalancer) && - route.LoadBalancerOptions.Type == loadBalancer.Type) // TODO Case insensitive? - ? new OkResponse(loadBalancer) - : GetResponse(route, config); - } - } - catch (Exception ex) - { - return new ErrorResponse( - new UnableToFindLoadBalancerError($"Unable to find load balancer for '{route.LoadBalancerKey}'. Exception: {ex};")); - } +using Ocelot.Configuration; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer; + +public class LoadBalancerHouse : ILoadBalancerHouse +{ + private readonly ILoadBalancerFactory _factory; + private readonly Dictionary _loadBalancers; +#if NET9_0_OR_GREATER + private static readonly Lock SyncRoot = new(); +#else + private static readonly object SyncRoot = new(); +#endif + + public LoadBalancerHouse(ILoadBalancerFactory factory) + { + _factory = factory; + _loadBalancers = new(); + } + + public Response Get(DownstreamRoute route, ServiceProviderConfiguration config) + { + try + { + lock (SyncRoot) + { + return _loadBalancers.TryGetValue(route.LoadBalancerKey, out var loadBalancer) && + loadBalancer.Type.Equals(route.LoadBalancerOptions.Type, StringComparison.OrdinalIgnoreCase) + ? new OkResponse(loadBalancer) + : GetResponse(route, config); + } + } + catch (Exception ex) + { + return new ErrorResponse( + new UnableToFindLoadBalancerError($"Unable to find load balancer for '{route.LoadBalancerKey}'. Exception: {ex};")); + } } private Response GetResponse(DownstreamRoute route, ServiceProviderConfiguration config) @@ -44,6 +50,6 @@ private Response GetResponse(DownstreamRoute route, ServiceProvid var balancer = result.Data; _loadBalancers[route.LoadBalancerKey] = balancer; // TODO TryAdd ? - return new OkResponse(balancer); - } -} + return new OkResponse(balancer); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/CouldNotFindLoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/CouldNotFindLoadBalancerCreator.cs deleted file mode 100644 index 8384c1e04..000000000 --- a/src/Ocelot/LoadBalancer/LoadBalancers/CouldNotFindLoadBalancerCreator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class CouldNotFindLoadBalancerCreator : Error -{ - public CouldNotFindLoadBalancerCreator(string message) - : base(message, OcelotErrorCode.CouldNotFindLoadBalancerCreator, 404) - { - } -} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ErrorInvokingLoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ErrorInvokingLoadBalancerCreator.cs deleted file mode 100644 index f3239748d..000000000 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ErrorInvokingLoadBalancerCreator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class ErrorInvokingLoadBalancerCreator : Error -{ - public ErrorInvokingLoadBalancerCreator(Exception e) : base($"Error when invoking user provided load balancer creator function, Message: {e.Message}, StackTrace: {e.StackTrace}", OcelotErrorCode.ErrorInvokingLoadBalancerCreator, 500) - { - } -} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs deleted file mode 100644 index 31d65d08b..000000000 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class ServicesAreEmptyError : Error -{ - public ServicesAreEmptyError(string message) - : base(message, OcelotErrorCode.ServicesAreEmptyError, 404) - { - } -} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs deleted file mode 100644 index ae8fae040..000000000 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.LoadBalancer.LoadBalancers; - -public class ServicesAreNullError : Error -{ - public ServicesAreNullError(string message) - : base(message, OcelotErrorCode.ServicesAreNullError, 404) - { - } -} diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/LoadBalancingMiddleware.cs similarity index 74% rename from src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs rename to src/Ocelot/LoadBalancer/LoadBalancingMiddleware.cs index b57ea876a..8df42fb1a 100644 --- a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancingMiddleware.cs @@ -1,74 +1,68 @@ using Microsoft.AspNetCore.Http; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Logging; using Ocelot.Middleware; - -namespace Ocelot.LoadBalancer.Middleware; - -public class LoadBalancingMiddleware : OcelotMiddleware -{ - private readonly RequestDelegate _next; - private readonly ILoadBalancerHouse _loadBalancerHouse; - - public LoadBalancingMiddleware(RequestDelegate next, - IOcelotLoggerFactory loggerFactory, - ILoadBalancerHouse loadBalancerHouse) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _loadBalancerHouse = loadBalancerHouse; - } - - public async Task Invoke(HttpContext httpContext) - { - var downstreamRoute = httpContext.Items.DownstreamRoute(); - - var internalConfiguration = httpContext.Items.IInternalConfiguration(); - - var loadBalancer = _loadBalancerHouse.Get(downstreamRoute, internalConfiguration.ServiceProviderConfiguration); - - if (loadBalancer.IsError) - { - Logger.LogDebug("there was an error retriving the loadbalancer, setting pipeline error"); - httpContext.Items.UpsertErrors(loadBalancer.Errors); - return; - } - - var hostAndPort = await loadBalancer.Data.LeaseAsync(httpContext); - if (hostAndPort.IsError) - { - Logger.LogDebug("there was an error leasing the loadbalancer, setting pipeline error"); - httpContext.Items.UpsertErrors(hostAndPort.Errors); - return; - } - - var downstreamRequest = httpContext.Items.DownstreamRequest(); - - //todo check downstreamRequest is ok - downstreamRequest.Host = hostAndPort.Data.DownstreamHost; - - if (hostAndPort.Data.DownstreamPort > 0) - { - downstreamRequest.Port = hostAndPort.Data.DownstreamPort; - } - - if (!string.IsNullOrEmpty(hostAndPort.Data.Scheme)) - { - downstreamRequest.Scheme = hostAndPort.Data.Scheme; - } - - try - { - await _next.Invoke(httpContext); - } - catch (Exception) - { - Logger.LogDebug("Exception calling next middleware, exception will be thrown to global handler"); - throw; - } - finally - { - loadBalancer.Data.Release(hostAndPort.Data); - } - } -} + +namespace Ocelot.LoadBalancer; + +public class LoadBalancingMiddleware : OcelotMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILoadBalancerHouse _loadBalancerHouse; + + public LoadBalancingMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + ILoadBalancerHouse loadBalancerHouse) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _loadBalancerHouse = loadBalancerHouse; + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + var internalConfiguration = httpContext.Items.IInternalConfiguration(); + + var loadBalancer = _loadBalancerHouse.Get(downstreamRoute, internalConfiguration.ServiceProviderConfiguration); + + if (loadBalancer.IsError) + { + httpContext.Items.UpsertErrors(loadBalancer.Errors); + return; + } + + var hostAndPort = await loadBalancer.Data.LeaseAsync(httpContext); + if (hostAndPort.IsError) + { + httpContext.Items.UpsertErrors(hostAndPort.Errors); + return; + } + + var downstreamRequest = httpContext.Items.DownstreamRequest(); + + //todo check downstreamRequest is ok + downstreamRequest.Host = hostAndPort.Data.DownstreamHost; + + if (hostAndPort.Data.DownstreamPort > 0) + { + downstreamRequest.Port = hostAndPort.Data.DownstreamPort; + } + + if (!string.IsNullOrEmpty(hostAndPort.Data.Scheme)) + { + downstreamRequest.Scheme = hostAndPort.Data.Scheme; + } + + try + { + // If an exception occurs, the object will be handled by the global exception handler + await _next.Invoke(httpContext); + } + finally + { + loadBalancer.Data.Release(hostAndPort.Data); + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs b/src/Ocelot/LoadBalancer/StickySession.cs similarity index 88% rename from src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs rename to src/Ocelot/LoadBalancer/StickySession.cs index b5cef15e9..be7e7bfcc 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs +++ b/src/Ocelot/LoadBalancer/StickySession.cs @@ -1,6 +1,6 @@ using Ocelot.Values; -namespace Ocelot.LoadBalancer.LoadBalancers; +namespace Ocelot.LoadBalancer; public class StickySession { diff --git a/src/Ocelot/Middleware/OcelotPipelineExtensions.cs b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs index 7286cda56..2f64e8619 100644 --- a/src/Ocelot/Middleware/OcelotPipelineExtensions.cs +++ b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs @@ -9,7 +9,7 @@ using Ocelot.DownstreamUrlCreator.Middleware; using Ocelot.Errors.Middleware; using Ocelot.Headers.Middleware; -using Ocelot.LoadBalancer.Middleware; +using Ocelot.LoadBalancer; using Ocelot.Multiplexer; using Ocelot.QueryStrings.Middleware; using Ocelot.RateLimiting; diff --git a/src/Ocelot/RateLimiting/RateLimiting.cs b/src/Ocelot/RateLimiting/RateLimiting.cs index ac45ea9da..53ce38b49 100644 --- a/src/Ocelot/RateLimiting/RateLimiting.cs +++ b/src/Ocelot/RateLimiting/RateLimiting.cs @@ -132,7 +132,7 @@ public virtual double RetryAfter(RateLimitCounter counter, RateLimitRule rule, D } // Counting Period is active - bool doNotWait = rule.WaitSpan == TimeSpan.Zero || rule.Wait.IsNullOrEmpty() || rule.Wait == RateLimitRule.ZeroWait; + bool doNotWait = rule.WaitSpan == TimeSpan.Zero || rule.Wait.IsEmpty() || rule.Wait == RateLimitRule.ZeroWait; if (doNotWait && counter.StartedAt + rule.PeriodSpan > now) { //return waitWindow.TotalSeconds - (now - exceededAt).TotalSeconds; // minus seconds past diff --git a/src/Ocelot/RateLimiting/RateLimitingMiddleware.cs b/src/Ocelot/RateLimiting/RateLimitingMiddleware.cs index 690b83f67..296d60ceb 100644 --- a/src/Ocelot/RateLimiting/RateLimitingMiddleware.cs +++ b/src/Ocelot/RateLimiting/RateLimitingMiddleware.cs @@ -49,12 +49,12 @@ public Task Invoke(HttpContext context) // Log warnings and break execution var rule = options.Rule; var warning = string.Empty; - if (identity.ClientId.IsNullOrEmpty()) // unknown client aka security check, so block unknown clients + if (identity.ClientId.IsEmpty()) // unknown client aka security check, so block unknown clients { warning = $"Rate limiting client could not be identified for the route '{downstreamRoute.Name(true)}' due to a missing or unknown client ID header required by rule '{rule}'!"; // and don't log the header name because of security } - if (!warning.IsNullOrEmpty()) + if (!warning.IsEmpty()) { Logger.LogWarning(warning); RateLimitOptions errorOpts = new(options) diff --git a/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs index 1888c3ac0..cdd28729d 100644 --- a/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs @@ -5,6 +5,8 @@ namespace Ocelot.ServiceDiscovery.Providers; public class ServiceFabricServiceDiscoveryProvider : IServiceDiscoveryProvider { + public const string Type = "ServiceFabric"; // TODO This property should be defined in the IServiceDiscoveryProvider interface + private readonly ServiceFabricConfiguration _configuration; public ServiceFabricServiceDiscoveryProvider(ServiceFabricConfiguration configuration) diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index f5ff8c329..e072ecab1 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -45,7 +45,7 @@ private Response GetServiceDiscoveryProvider(ServiceP { _logger.LogInformation(() => $"Getting service discovery provider of {nameof(config.Type)} '{config.Type}'..."); - if (config.Type?.ToLower() == "servicefabric") + if (ServiceFabricServiceDiscoveryProvider.Type.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, route.ServiceName); return new OkResponse(new ServiceFabricServiceDiscoveryProvider(sfConfig)); @@ -54,8 +54,7 @@ private Response GetServiceDiscoveryProvider(ServiceP if (_delegates != null) { var provider = _delegates?.Invoke(_provider, config, route); - - if (provider.GetType().Name.ToLower() == config.Type.ToLower()) + if (provider.GetType().Name.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { return new OkResponse(provider); } diff --git a/test/Ocelot.AcceptanceTests/AuthorizationTests.cs b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs index b7e18af9f..0a26233f5 100644 --- a/test/Ocelot.AcceptanceTests/AuthorizationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs @@ -60,7 +60,7 @@ public void Should_return_response_200_authorizing_route() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "Test", @@ -121,7 +121,7 @@ public void Should_return_response_403_authorizing_route() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "Test", @@ -200,7 +200,7 @@ public void Should_return_response_403_using_identity_server_with_scope_not_allo }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "Test", @@ -245,7 +245,7 @@ public void Should_fix_issue_240() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "Test", diff --git a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs index c2e36abae..21a3de9db 100644 --- a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs @@ -40,7 +40,7 @@ public void Should_forward_tracing_information_from_ocelot_and_downstream_servic }, }, UpstreamPathTemplate = "/api001/values", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], HttpHandlerOptions = new FileHttpHandlerOptions { UseTracing = true, @@ -59,7 +59,7 @@ public void Should_forward_tracing_information_from_ocelot_and_downstream_servic }, }, UpstreamPathTemplate = "/api002/values", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], HttpHandlerOptions = new FileHttpHandlerOptions { UseTracing = true, @@ -107,7 +107,7 @@ public void Should_return_tracing_header() }, }, UpstreamPathTemplate = "/api001/values", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], HttpHandlerOptions = new FileHttpHandlerOptions { UseTracing = true, diff --git a/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs b/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs index 76cac9d78..278616d3e 100644 --- a/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs +++ b/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs @@ -50,7 +50,7 @@ public void Should_throw_exception_if_cannot_start_because_service_discovery_pro DownstreamPathTemplate = "/", DownstreamScheme = "http", UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], ServiceName = "test", }, }, diff --git a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs index ed11b5662..1786cf13f 100644 --- a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs @@ -30,7 +30,7 @@ public void Should_return_response_200_when_global_ignore_case_sensitivity_set() }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; @@ -64,7 +64,7 @@ public void Should_return_response_200_when_route_ignore_case_sensitivity_set() }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = false, }, }, @@ -99,7 +99,7 @@ public void Should_return_response_404_when_route_respect_case_sensitivity_set() }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = true, }, }, @@ -134,7 +134,7 @@ public void Should_return_response_200_when_route_respect_case_sensitivity_set() }, DownstreamScheme = "http", UpstreamPathTemplate = "/PRODUCTS/{productId}", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = true, }, }, @@ -169,7 +169,7 @@ public void Should_return_response_404_when_global_respect_case_sensitivity_set( }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = true, }, }, @@ -204,7 +204,7 @@ public void Should_return_response_200_when_global_respect_case_sensitivity_set( }, DownstreamScheme = "http", UpstreamPathTemplate = "/PRODUCTS/{productId}", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = true, }, }, diff --git a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs index ee2c8fece..d72d6471c 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs @@ -55,7 +55,7 @@ public void Should_return_200_and_change_downstream_path() }, DownstreamScheme = "http", UpstreamPathTemplate = "/users/{userId}", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKeys = ["Test"], diff --git a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs index aecdbbfbc..89c95792a 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs @@ -64,7 +64,7 @@ public void Should_return_response_200_and_foward_claim_as_header() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "Test", diff --git a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs index 42e2e7150..9d1a32a47 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs @@ -65,7 +65,7 @@ public void Should_return_response_200_and_foward_claim_as_query_string() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "Test", @@ -130,7 +130,7 @@ public void Should_return_response_200_and_foward_claim_as_query_string_and_pres }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "Test", diff --git a/test/Ocelot.AcceptanceTests/ConcurrentSteps.cs b/test/Ocelot.AcceptanceTests/ConcurrentSteps.cs index d43d8c29f..e791f872e 100644 --- a/test/Ocelot.AcceptanceTests/ConcurrentSteps.cs +++ b/test/Ocelot.AcceptanceTests/ConcurrentSteps.cs @@ -102,6 +102,7 @@ public static class HeaderNames public const string Host = nameof(Uri.Host); public const string Port = nameof(Uri.Port); public const string Counter = nameof(Counter); + public const string Path = nameof(Path); } protected RequestDelegate MapGet(int index, string body) => MapGet(index, body, HttpStatusCode.OK); @@ -126,6 +127,7 @@ protected RequestDelegate MapGet(int index, string body, HttpStatusCode successC response.Headers.Append(HeaderNames.Host, new StringValues(request.Host.Host)); response.Headers.Append(HeaderNames.Port, new StringValues(request.Host.Port.ToString())); response.Headers.Append(HeaderNames.Counter, new StringValues(count.ToString())); + response.Headers.Append(HeaderNames.Path, new StringValues(request.Path + request.QueryString)); await response.WriteAsync(responseBody); } catch (Exception exception) @@ -266,4 +268,18 @@ public void ThenServiceCountersShouldMatchLeasingCounters(ILoadBalancerAnalyzer } } } + + protected IEnumerable ThenAllResponsesHeaderExists(string key) + { + foreach (var kv in _responses) + { + var response = kv.Value.ShouldNotBeNull(); + response.Headers.Contains(key).ShouldBeTrue(); + var header = response.Headers.GetValues(key); + yield return string.Join(';', header); + } + } + + protected virtual string ServiceName([CallerMemberName] string serviceName = null) => serviceName ?? GetType().Name; + protected virtual string ServiceNamespace() => GetType().Namespace; } diff --git a/test/Ocelot.AcceptanceTests/Core/LoadTests.cs b/test/Ocelot.AcceptanceTests/Core/LoadTests.cs index 903bd3ee8..1bc825ede 100644 --- a/test/Ocelot.AcceptanceTests/Core/LoadTests.cs +++ b/test/Ocelot.AcceptanceTests/Core/LoadTests.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; using System.Diagnostics; namespace Ocelot.AcceptanceTests.Core; diff --git a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs index 8f3518a65..4ff8a5d72 100644 --- a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs +++ b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs @@ -47,7 +47,7 @@ public void Should_call_pre_query_string_builder_middleware() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; @@ -92,7 +92,7 @@ public void Should_call_authorization_middleware() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; @@ -137,7 +137,7 @@ public void Should_call_authentication_middleware() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; @@ -182,7 +182,7 @@ public void Should_call_pre_error_middleware() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; @@ -227,7 +227,7 @@ public void Should_call_pre_authorization_middleware() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; @@ -271,7 +271,7 @@ public void Should_call_pre_http_authentication_middleware() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; @@ -317,7 +317,7 @@ public void Should_not_throw_when_pipeline_terminates_early() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; @@ -363,7 +363,7 @@ public void Should_fix_issue_237() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; diff --git a/test/Ocelot.AcceptanceTests/StickySessionsTests.cs b/test/Ocelot.AcceptanceTests/LoadBalancer/CookieStickySessionsTests.cs similarity index 57% rename from test/Ocelot.AcceptanceTests/StickySessionsTests.cs rename to test/Ocelot.AcceptanceTests/LoadBalancer/CookieStickySessionsTests.cs index d9277dd93..141ed7c4a 100644 --- a/test/Ocelot.AcceptanceTests/StickySessionsTests.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancer/CookieStickySessionsTests.cs @@ -1,152 +1,188 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; using System.Runtime.CompilerServices; - -namespace Ocelot.AcceptanceTests; - -public sealed class StickySessionsTests : Steps -{ - private readonly int[] _counters; -#if NET9_0_OR_GREATER - private static readonly Lock SyncLock = new(); -#else - private static readonly object SyncLock = new(); -#endif - - public StickySessionsTests() : base() - { - _counters = new int[2]; - } - - [Fact] - [Trait("Feat", "336")] - public void ShouldUseSameDownstreamHost_ForSingleRouteWithHighLoad() - { - var port1 = PortFinder.GetRandomPort(); - var port2 = PortFinder.GetRandomPort(); - var route = GivenRoute("/") - .WithHosts(Localhost(port1), Localhost(port2)); - var cookieName = route.LoadBalancerOptions.Key; - var configuration = GivenConfiguration(route); - - this.Given(x => x.GivenProductServiceIsRunning(0, port1)) - .Given(x => x.GivenProductServiceIsRunning(1, port2)) - .And(_ => GivenThereIsAConfiguration(configuration)) - .And(_ => GivenOcelotIsRunning()) - .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10, cookieName, Guid.NewGuid().ToString())) - .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 10)) // RoundRobin should return first service with port1 - .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 0)) - .BDDfy(); - } - - [Fact] - [Trait("Feat", "336")] - public void ShouldUseDifferentDownstreamHost_ForDoubleRoutesWithDifferentCookies() - { - var port1 = PortFinder.GetRandomPort(); - var port2 = PortFinder.GetRandomPort(); - var route1 = GivenRoute("/") - .WithHosts(Localhost(port1), Localhost(port2)); - var cookieName = route1.LoadBalancerOptions.Key; - var route2 = GivenRoute("/test", cookieName + "bestid") - .WithHosts(Localhost(port2), Localhost(port1)); - var configuration = GivenConfiguration(route1, route2); - - this.Given(x => x.GivenProductServiceIsRunning(0, port1)) - .Given(x => x.GivenProductServiceIsRunning(1, port2)) - .And(_ => GivenThereIsAConfiguration(configuration)) - .And(_ => GivenOcelotIsRunning()) - .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/", cookieName, "123")) // both cookies should have different values - .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/test", cookieName + "bestid", "123")) // stick by cookie value - .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 1)) - .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 1)) - .BDDfy(); - } - - [Fact] - [Trait("Feat", "336")] - public void ShouldUseSameDownstreamHost_ForDifferentRoutesWithSameCookie() - { - var port1 = PortFinder.GetRandomPort(); - var port2 = PortFinder.GetRandomPort(); - var route1 = GivenRoute("/") - .WithHosts(Localhost(port1), Localhost(port2)); - var cookieName = route1.LoadBalancerOptions.Key; - var route2 = GivenRoute("/test", cookieName) - .WithHosts(Localhost(port2), Localhost(port1)); - var configuration = GivenConfiguration(route1, route2); - - this.Given(x => x.GivenProductServiceIsRunning(0, port1)) - .Given(x => x.GivenProductServiceIsRunning(1, port2)) - .And(_ => GivenThereIsAConfiguration(configuration)) - .And(_ => GivenOcelotIsRunning()) - .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/", cookieName, "123")) - .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/test", cookieName, "123")) - .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 2)) - .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 0)) - .BDDfy(); - } - - private static FileRoute GivenRoute(string upstream, [CallerMemberName] string cookieName = null) => new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - UpstreamPathTemplate = upstream ?? "/", - UpstreamHttpMethod = [HttpMethods.Get], - LoadBalancerOptions = new() - { - Type = nameof(CookieStickySessions), - Key = cookieName, // !!! - Expiry = 300000, - }, - }; - - private Task WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) - { - var tasks = new Task[times]; - for (var i = 0; i < times; i++) - { - tasks[i] = GetParallelTask(url, cookie, value); - } - - return Task.WhenAll(tasks); - } - - private async Task GetParallelTask(string url, string cookie, string value) - { + +namespace Ocelot.AcceptanceTests.LoadBalancer; + +[Trait("Feat", "336")] // https://github.com/ThreeMammals/Ocelot/pull/336 +public sealed class CookieStickySessionsTests : Steps +{ + private int[] _counters; +#if NET9_0_OR_GREATER + private static readonly Lock SyncLock = new(); +#else + private static readonly object SyncLock = new(); +#endif + + public CookieStickySessionsTests() : base() + { + _counters = new int[2]; + } + + [Fact] + public void ShouldUseSameDownstreamHost_ForSingleRouteWithHighLoad() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var route = GivenStickySessionsRoute([port1, port2]); + var cookieName = route.LoadBalancerOptions.Key; + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenProductServiceIsRunning(0, port1)) + .Given(x => x.GivenProductServiceIsRunning(1, port2)) + .And(_ => GivenThereIsAConfiguration(configuration)) + .And(_ => GivenOcelotIsRunning()) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10, cookieName, Guid.NewGuid().ToString())) + .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 10)) // RoundRobin should return first service with port1 + .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 0)) + .BDDfy(); + } + + [Fact] + public void ShouldUseDifferentDownstreamHost_ForDoubleRoutesWithDifferentCookies() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var route1 = GivenStickySessionsRoute([port1, port2]); + var cookieName = route1.LoadBalancerOptions.Key; + var route2 = GivenStickySessionsRoute([port2, port1], "/test", cookieName + "bestid"); + var configuration = GivenConfiguration(route1, route2); + + this.Given(x => x.GivenProductServiceIsRunning(0, port1)) + .Given(x => x.GivenProductServiceIsRunning(1, port2)) + .And(_ => GivenThereIsAConfiguration(configuration)) + .And(_ => GivenOcelotIsRunning()) + .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/", cookieName, "123")) // both cookies should have different values + .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/test", cookieName + "bestid", "123")) // stick by cookie value + .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 1)) + .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 1)) + .BDDfy(); + } + + [Fact] + public void ShouldUseSameDownstreamHost_ForDifferentRoutesWithSameCookie() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var route1 = GivenStickySessionsRoute([port1, port2]); + var cookieName = route1.LoadBalancerOptions.Key; + var route2 = GivenStickySessionsRoute([port2, port1], "/test", cookieName); + var configuration = GivenConfiguration(route1, route2); + + this.Given(x => x.GivenProductServiceIsRunning(0, port1)) + .Given(x => x.GivenProductServiceIsRunning(1, port2)) + .And(_ => GivenThereIsAConfiguration(configuration)) + .And(_ => GivenOcelotIsRunning()) + .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/", cookieName, "123")) + .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/test", cookieName, "123")) + .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 2)) + .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 0)) + .BDDfy(); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + public async Task ShouldUseGlobalOptions_ForStaticRoutes() + { + _counters = new int[5]; + var ports = PortFinder.GetPorts(2); + var route1 = GivenStickySessionsRoute(ports); + route1.LoadBalancerOptions = new(); // no load balancing -> use global opts + var route2 = GivenStickySessionsRoute(ports.Reverse().ToArray(), "/test"); + route1.LoadBalancerOptions = new(); // no load balancing -> use global opts + var ports2 = PortFinder.GetPorts(2); + var route3 = GivenStickySessionsRoute(ports2, "/nextSticky", CookieName() + "-nextSticky"); + var port5 = PortFinder.GetRandomPort(); + var route4 = GivenStickySessionsRoute([port5], "/noLoadBalancing"); // this route should not be overwritten by global LB opts + route4.LoadBalancerOptions.Type = nameof(NoLoadBalancer); + + var configuration = GivenConfiguration(route1, route2, route3, route4); // static routes come to Routes collection + configuration.GlobalConfiguration.LoadBalancerOptions = new() + { + Type = nameof(CookieStickySessions), + Key = CookieName(), // !!! + }; + GivenProductServiceIsRunning(0, ports[0]); + GivenProductServiceIsRunning(1, ports[1]); + GivenProductServiceIsRunning(2, ports2[0]); + GivenProductServiceIsRunning(3, ports2[1]); + GivenProductServiceIsRunning(4, port5); + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunning(); + await WhenIGetUrlOnTheApiGatewayWithCookie("/", CookieName(), "123"); + await WhenIGetUrlOnTheApiGatewayWithCookie("/test", CookieName(), "123"); + await WhenIGetUrlOnTheApiGatewayMultipleTimes("/nextSticky", 5, CookieName() + "-nextSticky", "333"); + await WhenIGetUrlOnTheApiGatewayMultipleTimes("/noLoadBalancing", 7, "bla-bla-cookie", "bla-bla-value"); + ThenServiceShouldHaveBeenCalledTimes(0, 2); + ThenServiceShouldHaveBeenCalledTimes(1, 0); + ThenServiceShouldHaveBeenCalledTimes(2, 5); + ThenServiceShouldHaveBeenCalledTimes(3, 0); + ThenServiceShouldHaveBeenCalledTimes(4, 7); + } + + private static string CookieName([CallerMemberName] string cookieName = nameof(CookieStickySessionsTests)) => cookieName; + + private FileRoute GivenStickySessionsRoute(int[] ports, string upstream = null, [CallerMemberName] string cookieName = null) + { + var route = GivenRoute(ports[0], upstream: upstream ?? "/"); + route.DownstreamHostAndPorts = ports.Select(Localhost).ToList(); + route.LoadBalancerOptions = new() + { + Type = nameof(CookieStickySessions), + Key = cookieName, // !!! + Expiry = 300_000, + }; + return route; + } + + private Task WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) + { + var tasks = new Task[times]; + for (var i = 0; i < times; i++) + { + tasks[i] = GetParallelTask(url, cookie, value); + } + + return Task.WhenAll(tasks); + } + + private async Task GetParallelTask(string url, string cookie, string value) + { var response = await WhenIGetUrlOnTheApiGateway(url, cookie, value); - var content = await response.Content.ReadAsStringAsync(); - var count = int.Parse(content); - count.ShouldBeGreaterThan(0); - } - - private void ThenServiceShouldHaveBeenCalledTimes(int index, int times) - { - _counters[index].ShouldBe(times); - } - - private void GivenProductServiceIsRunning(int index, int port) - { - handler.GivenThereIsAServiceRunningOn(port, async context => - { - try - { - string response; - lock (SyncLock) - { - _counters[index]++; - response = _counters[index].ToString(); - } - - context.Response.StatusCode = (int)HttpStatusCode.OK; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } -} + var content = await response.Content.ReadAsStringAsync(); + var count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } + + private void ThenServiceShouldHaveBeenCalledTimes(int index, int times) + { + _counters[index].ShouldBe(times); + } + + private void GivenProductServiceIsRunning(int index, int port) + { + handler.GivenThereIsAServiceRunningOn(port, async context => + { + try + { + string response; + lock (SyncLock) + { + _counters[index]++; + response = _counters[index].ToString(); + } + + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } +} diff --git a/test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzer.cs b/test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzer.cs index 79ee9cf72..534781683 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzer.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzer.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; diff --git a/test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzerCreator.cs b/test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzerCreator.cs index 785189ce2..791ef1ed7 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzerCreator.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzerCreator.cs @@ -1,5 +1,5 @@ using Ocelot.Configuration; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; diff --git a/test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerAnalyzer.cs b/test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerAnalyzer.cs index dc19a51e4..cf9b0ab52 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerAnalyzer.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerAnalyzer.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; using System.Collections.Concurrent; @@ -113,6 +114,7 @@ public virtual int TopOfConnections() } public virtual string Type => nameof(LoadBalancerAnalyzer); - public virtual Task> LeaseAsync(HttpContext httpContext) => Task.FromResult>(new ErrorResponse(new UnableToFindLoadBalancerError(GetType().Name))); + public virtual Task> LeaseAsync(HttpContext httpContext) + => Task.FromResult>(new ErrorResponse(new UnableToFindLoadBalancerError(GetType().Name))); public virtual void Release(ServiceHostAndPort hostAndPort) { } } diff --git a/test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerTests.cs b/test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerTests.cs index 73314c8a8..8c2809a7d 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerTests.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerTests.cs @@ -3,7 +3,8 @@ using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; @@ -19,7 +20,7 @@ public sealed class LoadBalancerTests : ConcurrentSteps public void ShouldLoadBalanceRequestWithLeastConnection(bool withAnalyzer) { var ports = PortFinder.GetPorts(2); - var route = GivenRoute(withAnalyzer ? nameof(LeastConnectionAnalyzer) : nameof(LeastConnection), ports); + var route = GivenLbRoute(ports, withAnalyzer ? nameof(LeastConnectionAnalyzer) : nameof(LeastConnection)); var configuration = GivenConfiguration(route); var downstreamServiceUrls = ports.Select(DownstreamUrl).ToArray(); LeastConnectionAnalyzer lbAnalyzer = null; @@ -49,7 +50,7 @@ LeastConnectionAnalyzer getAnalyzer(DownstreamRoute route, IServiceDiscoveryProv public void ShouldLoadBalanceRequestWithRoundRobin(bool withAnalyzer) { var ports = PortFinder.GetPorts(2); - var route = GivenRoute(withAnalyzer ? nameof(RoundRobinAnalyzer) : nameof(RoundRobin), ports); + var route = GivenLbRoute(ports, withAnalyzer ? nameof(RoundRobinAnalyzer) : nameof(RoundRobin)); var configuration = GivenConfiguration(route); var downstreamServiceUrls = ports.Select(DownstreamUrl).ToArray(); RoundRobinAnalyzer lbAnalyzer = null; @@ -79,7 +80,7 @@ public void ShouldLoadBalanceRequestWithCustomLoadBalancer() Func loadBalancerFactoryFunc = (serviceProvider, route, discoveryProvider) => new CustomLoadBalancer(discoveryProvider.GetAsync); var ports = PortFinder.GetPorts(2); - var route = GivenRoute(nameof(CustomLoadBalancer), ports); + var route = GivenLbRoute(ports, nameof(CustomLoadBalancer)); var configuration = GivenConfiguration(route); var downstreamServiceUrls = ports.Select(DownstreamUrl).ToArray(); Action withCustomLoadBalancer = (s) @@ -94,6 +95,87 @@ public void ShouldLoadBalanceRequestWithCustomLoadBalancer() .BDDfy(); } + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + public void ShouldApplyGlobalOptions_ForStaticRoutes() + { + var ports1 = PortFinder.GetPorts(2); + var route1 = GivenLbRoute(ports1, upstream: "/route1"); + route1.LoadBalancerOptions = new(); // no load balancing -> use global opts + var ports2 = PortFinder.GetPorts(2); + var route2 = GivenLbRoute(ports2, nameof(LeastConnection), "/route2"); + var ports3 = PortFinder.GetPorts(2); + var route3 = GivenLbRoute(ports3, nameof(NoLoadBalancer), "/noLoadBalancing"); + + var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection + configuration.GlobalConfiguration.LoadBalancerOptions = new(nameof(RoundRobin)); + + var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); + GivenMultipleServiceInstancesAreRunning(downstreamUrls); + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunning(); + + WhenIGetUrlOnTheApiGatewayConcurrently("/route1", 2); + WhenIGetUrlOnTheApiGatewayConcurrently("/route2", 5); + WhenIGetUrlOnTheApiGatewayConcurrently("/noLoadBalancing", 7); + + ThenServicesShouldHaveBeenCalledTimes(1, 1, 3, 2, 7, 0); // main assertion, explanation is below + ThenServiceShouldHaveBeenCalledTimes(0, 1); // RoundRobin for 2 + ThenServiceShouldHaveBeenCalledTimes(1, 1); // RoundRobin for 2 + ThenServiceShouldHaveBeenCalledTimes(2, 3); // LeastConnection for 5 + ThenServiceShouldHaveBeenCalledTimes(3, 2); // LeastConnection for 5 + ThenServiceShouldHaveBeenCalledTimes(4, 7); // NoLoadBalancer for 7 + ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 7 + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + public void ShouldApplyGlobalGroupOptions_ForStaticRoutes_WhenRouteOptsHasAKey() + { + // 1st route + var ports1 = PortFinder.GetPorts(2); + var route1 = GivenLbRoute(ports1, upstream: "/route1"); + route1.LoadBalancerOptions = null; // 1st route is not balanced + route1.Key = null; // 1st route is not in the global group + + // 2nd route + var ports2 = PortFinder.GetPorts(2); + var route2 = GivenLbRoute(ports2, upstream: "/route2"); + route2.LoadBalancerOptions = null; // 2nd route opts will be applied from global ones + route2.Key = "R2"; // 2nd route is in the group + + // 3rd route + var ports3 = PortFinder.GetPorts(2); + var route3 = GivenLbRoute(ports3, nameof(NoLoadBalancer), "/noLoadBalancing"); + + var configuration = GivenConfiguration(route1, route2, route3); + configuration.GlobalConfiguration.LoadBalancerOptions = new() + { + RouteKeys = ["R2"], + Type = nameof(RoundRobin), + }; + + var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); + GivenMultipleServiceInstancesAreRunning(downstreamUrls); + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunning(); + + WhenIGetUrlOnTheApiGatewayConcurrently("/route1", 2); + WhenIGetUrlOnTheApiGatewayConcurrently("/route2", 4); + WhenIGetUrlOnTheApiGatewayConcurrently("/noLoadBalancing", 5); + ThenServicesShouldHaveBeenCalledTimes(2, 0, 2, 2, 5, 0); // main assertion, explanation is below + ThenServiceShouldHaveBeenCalledTimes(0, 2); // NoLoadBalancer for 2 + ThenServiceShouldHaveBeenCalledTimes(1, 0); // NoLoadBalancer for 2 + ThenServiceShouldHaveBeenCalledTimes(2, 2); // RoundRobin for 4 + ThenServiceShouldHaveBeenCalledTimes(3, 2); // RoundRobin for 4 + ThenServiceShouldHaveBeenCalledTimes(4, 5); // NoLoadBalancer for 5 + ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 5 + } + private sealed class CustomLoadBalancer : ILoadBalancer { private readonly Func>> _services; @@ -117,13 +199,11 @@ public async Task> LeaseAsync(HttpContext httpConte public void Release(ServiceHostAndPort hostAndPort) { } } - private FileRoute GivenRoute(string loadBalancer, params int[] ports) => new() + private FileRoute GivenLbRoute(int[] ports, string loadBalancer = null, string upstream = null) { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = [HttpMethods.Get], - LoadBalancerOptions = new() { Type = loadBalancer ?? nameof(LeastConnection) }, - DownstreamHostAndPorts = ports.Select(Localhost).ToList(), - }; + var route = GivenRoute(ports[0], upstream: upstream); + route.DownstreamHostAndPorts = ports.Select(Localhost).ToList(); + route.LoadBalancerOptions = new(loadBalancer ?? nameof(LeastConnection)); + return route; + } } diff --git a/test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzer.cs b/test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzer.cs index 8f5f479ee..9bea1a48a 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzer.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzer.cs @@ -1,7 +1,8 @@ using KubeClient.Models; using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; diff --git a/test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzerCreator.cs b/test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzerCreator.cs index a8f7a2c44..41aff7328 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzerCreator.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzerCreator.cs @@ -1,5 +1,5 @@ using Ocelot.Configuration; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; diff --git a/test/Ocelot.AcceptanceTests/LogLevelTests.cs b/test/Ocelot.AcceptanceTests/LogLevelTests.cs index 969dcbf2b..d067f2733 100644 --- a/test/Ocelot.AcceptanceTests/LogLevelTests.cs +++ b/test/Ocelot.AcceptanceTests/LogLevelTests.cs @@ -64,7 +64,7 @@ private void TestFactory(string[] notAllowedMessageTypes, string[] allowedMessag }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], RequestIdKey = "Oc-RequestId", }, }, diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 30d8c279c..613d6210a 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -42,7 +42,7 @@ - + diff --git a/test/Ocelot.AcceptanceTests/OpenTracingTests.cs b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs index 80450919e..52bec9a4f 100644 --- a/test/Ocelot.AcceptanceTests/OpenTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs @@ -43,7 +43,7 @@ public void Should_forward_tracing_information_from_ocelot_and_downstream_servic }, }, UpstreamPathTemplate = "/api001/values", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], HttpHandlerOptions = new FileHttpHandlerOptions { UseTracing = true, @@ -62,7 +62,7 @@ public void Should_forward_tracing_information_from_ocelot_and_downstream_servic }, }, UpstreamPathTemplate = "/api002/values", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], HttpHandlerOptions = new FileHttpHandlerOptions { UseTracing = true, @@ -109,7 +109,7 @@ public void Should_return_tracing_header() }, }, UpstreamPathTemplate = "/api001/values", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], HttpHandlerOptions = new FileHttpHandlerOptions { UseTracing = true, diff --git a/test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs b/test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs index e5ca16b2d..e338a6b9e 100644 --- a/test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Http; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; using System.Web; namespace Ocelot.AcceptanceTests.Routing; @@ -144,7 +144,7 @@ public void Should_fix_issue_134() .WithMethods(methods); var route2 = GivenRoute(port, "/vacancy/{vacancyId}", "/api/v1/vacancy/{vacancyId}") .WithMethods(methods); - route1.LoadBalancerOptions.Type = route2.LoadBalancerOptions.Type = nameof(LeastConnection); + route1.LoadBalancerOptions = route2.LoadBalancerOptions = new(nameof(LeastConnection)); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/v1/vacancy/1", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) @@ -555,7 +555,7 @@ public void Should_return_404_when_calling_upstream_route_with_no_matching_downs var methods = new string[] { HttpMethods.Options, HttpMethods.Put, HttpMethods.Get, HttpMethods.Post, HttpMethods.Delete }; var route1 = GivenRoute(port, "/vacancy/", "/api/v1/vacancy").WithMethods(methods); var route2 = GivenRoute(port, "/vacancy/{vacancyId}", "/api/v1/vacancy/{vacancyId}").WithMethods(methods); - route1.LoadBalancerOptions.Type = route2.LoadBalancerOptions.Type = nameof(LeastConnection); + route1.LoadBalancerOptions = route2.LoadBalancerOptions = new(nameof(LeastConnection)); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/v1/vacancy/1", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs index ab80c7dc6..38452d201 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs @@ -44,7 +44,7 @@ public void Should_return_response_200_with_simple_url() }, }, UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, GlobalConfiguration = new FileGlobalConfiguration @@ -102,7 +102,7 @@ public void Should_load_configuration_out_of_consul() }, }, UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"}, + UpstreamHttpMethod = ["Get"], }, }, GlobalConfiguration = new FileGlobalConfiguration @@ -162,7 +162,7 @@ public void Should_load_configuration_out_of_consul_if_it_is_changed() }, }, UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"}, + UpstreamHttpMethod = ["Get"], }, }, GlobalConfiguration = new FileGlobalConfiguration @@ -193,7 +193,7 @@ public void Should_load_configuration_out_of_consul_if_it_is_changed() }, }, UpstreamPathTemplate = "/cs/status/awesome", - UpstreamHttpMethod = new List {"Get"}, + UpstreamHttpMethod = ["Get"], }, }, GlobalConfiguration = new FileGlobalConfiguration @@ -240,7 +240,7 @@ public void Should_handle_request_to_consul_for_downstream_service_and_make_requ var consulConfig = new FileConfiguration { - DynamicRoutes = new List + DynamicRoutes = new() { new() { @@ -255,9 +255,9 @@ public void Should_handle_request_to_consul_for_downstream_service_and_make_requ }, }, }, - GlobalConfiguration = new FileGlobalConfiguration + GlobalConfiguration = new() { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + ServiceDiscoveryProvider = new() { Scheme = "http", Host = "localhost", @@ -276,9 +276,9 @@ public void Should_handle_request_to_consul_for_downstream_service_and_make_requ var configuration = new FileConfiguration { - GlobalConfiguration = new FileGlobalConfiguration + GlobalConfiguration = new() { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + ServiceDiscoveryProvider = new() { Scheme = "http", Host = "localhost", @@ -286,18 +286,18 @@ public void Should_handle_request_to_consul_for_downstream_service_and_make_requ }, }, }; - + var upstreamPath = $"/{serviceName}/something"; this.Given(x => x.GivenThereIsAServiceRunningOn(servicePort, "/something", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenTheConsulConfigurationIs(consulConfig)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, serviceName)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/web/something", 1)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes(upstreamPath, 1)) .Then(x => ThenTheStatusCodeShouldBe(200)) - .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/web/something", 2)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes(upstreamPath, 2)) .Then(x => ThenTheStatusCodeShouldBe(200)) - .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/web/something", 1)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes(upstreamPath, 1)) .Then(x => ThenTheStatusCodeShouldBe(428)) .BDDfy(); } diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs index f9c4a3d8a..67dc2a9df 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -8,7 +8,9 @@ using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Infrastructure; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Creators; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Logging; using Ocelot.Provider.Consul; using Ocelot.Provider.Consul.Interfaces; @@ -94,7 +96,8 @@ public void ShouldHandleRequestToConsulForDownstreamServiceAndMakeRequest() [Fact] [Trait("Bug", "213")] - [Trait("Feat", "201 340")] + [Trait("Feat", "201")] + [Trait("Feat", "340")] public void ShouldHandleRequestToConsulForDownstreamServiceAndMakeRequestWhenDynamicRoutingWithNoRoutes() { const string serviceName = "web"; @@ -250,8 +253,9 @@ private async Task WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string } [Theory] + [Trait("Bug", "849")] + [Trait("Bug", "1496")] [Trait("PR", "1944")] - [Trait("Bugs", "849 1496")] [InlineData(nameof(NoLoadBalancer))] [InlineData(nameof(RoundRobin))] [InlineData(nameof(LeastConnection))] @@ -379,7 +383,7 @@ public void ShouldReturnDifferentServicesWhenThereAre2SequentialRequestsToDiffer var service2 = GivenServiceEntry(ports[1], serviceName: Bug2119ServiceNames[1]); var route1 = GivenRoute("/{all}", "/projects/{all}", serviceName: Bug2119ServiceNames[0], loadBalancerType: loadBalancer); var route2 = GivenRoute("/{all}", "/customers/{all}", serviceName: Bug2119ServiceNames[1], loadBalancerType: loadBalancer); - route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new List() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete }; + route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete }; var configuration = GivenServiceDiscovery(consulPort, route1, route2); var urls = ports.Select(DownstreamUrl).ToArray(); this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, Bug2119ServiceNames)) @@ -422,7 +426,7 @@ public void ShouldReturnDifferentServicesWhenSequentiallyRequestingToDifferentSe var service2 = GivenServiceEntry(ports[1], serviceName: Bug2119ServiceNames[1]); var route1 = GivenRoute("/{all}", "/projects/{all}", serviceName: Bug2119ServiceNames[0], loadBalancerType: loadBalancer); var route2 = GivenRoute("/{all}", "/customers/{all}", serviceName: Bug2119ServiceNames[1], loadBalancerType: loadBalancer); - route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new List() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete }; + route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete }; var configuration = GivenServiceDiscovery(consulPort, route1, route2); var urls = ports.Select(DownstreamUrl).ToArray(); Func requestToProjectsAndThenRequestToCustomersAndAssert = async (i) => @@ -472,7 +476,7 @@ public void ShouldReturnDifferentServicesWhenConcurrentlyRequestingToDifferentSe var service2 = GivenServiceEntry(ports[1], serviceName: Bug2119ServiceNames[1]); var route1 = GivenRoute("/{all}", "/projects/{all}", serviceName: Bug2119ServiceNames[0], loadBalancerType: loadBalancer); var route2 = GivenRoute("/{all}", "/customers/{all}", serviceName: Bug2119ServiceNames[1], loadBalancerType: loadBalancer); - route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new List() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete }; + route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete }; var configuration = GivenServiceDiscovery(consulPort, route1, route2); var urls = ports.Select(DownstreamUrl).ToArray(); this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, Bug2119ServiceNames)) // service names as responses @@ -491,6 +495,35 @@ public void ShouldReturnDifferentServicesWhenConcurrentlyRequestingToDifferentSe .BDDfy(); } + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + public void ShouldApplyGlobalLoadBalancerOptions_ForAllDynamicRoutes() + { + var ports = PortFinder.GetPorts(5); + var serviceName = ServiceName(); + var serviceEntries = ports.Select(port => GivenServiceEntry(port, serviceName: serviceName)).ToArray(); + var consulPort = PortFinder.GetRandomPort(); + var configuration = GivenServiceDiscovery(consulPort); + configuration.GlobalConfiguration.LoadBalancerOptions = new(nameof(RoundRobin)); + configuration.GlobalConfiguration.DownstreamScheme = Uri.UriSchemeHttp; + configuration.Routes = []; // dynamic routing + configuration.DynamicRoutes = []; // no dynamic routes, for ALL dynamic routes + + var urls = ports.Select(DownstreamUrl).ToArray(); + this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, serviceName)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntries)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning(WithConsul)) + .When(x => WhenIGetUrlOnTheApiGatewayConcurrently($"/{serviceName}/", 50)) + .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(50)) + .And(x => ThenAllServicesCalledRealisticAmountOfTimes(9, 11)) // soft assertion + .And(x => ThenServicesShouldHaveBeenCalledTimes(10, 10, 10, 10, 10)) // distribution by RoundRobin algorithm, aka strict assertion + .BDDfy(); + } + private Action WithLbAnalyzer(string loadBalancer) => loadBalancer switch { nameof(LeastConnection) => WithLbAnalyzer, @@ -555,7 +588,7 @@ public MyConsulServiceBuilder(IHttpContextAccessor contextAccessor, IConsulClien DownstreamPathTemplate = downstream ?? "/", DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = upstream ?? "/", - UpstreamHttpMethod = httpMethods != null ? new List(httpMethods) : [HttpMethods.Get], + UpstreamHttpMethod = httpMethods != null ? new(httpMethods) : [HttpMethods.Get], UpstreamHost = upstreamHost, ServiceName = serviceName, LoadBalancerOptions = new() diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/DynamicRoutingTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/DynamicRoutingTests.cs new file mode 100644 index 000000000..fff38eb6f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/DynamicRoutingTests.cs @@ -0,0 +1,195 @@ +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Infrastructure.Extensions; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.Metadata; +using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; + +namespace Ocelot.AcceptanceTests.ServiceDiscovery; + +/// +/// These tests are based on the custom service discovery provider, abstracting from currently implemented discovery providers and focusing on the dynamic routing features. +/// +public class DynamicRoutingTests : ConcurrentSteps +{ + [Fact] + [Trait("Feat", "351")] + public void ShouldForwardQueryStringToDownstream() + { + var ports = PortFinder.GetPorts(2); + var serviceName = ServiceName(); + var serviceUrls = ports.Select(DownstreamUrl).ToArray(); + var configuration = GivenDynamicRouting(new() + { + { serviceName, serviceUrls }, + }); + GivenMultipleServiceInstancesAreRunning(serviceUrls); + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunning(WithDiscovery); + var pathWithQueryString = $"/{serviceName}/?{nameof(TestID)}={TestID}"; + WhenIGetUrlOnTheApiGatewayConcurrently(pathWithQueryString, 2); + ThenAllServicesShouldHaveBeenCalledTimes(2); + ThenServicesShouldHaveBeenCalledTimes(1, 1); + var pathAndQuery = ThenAllResponsesHeaderExists(HeaderNames.Path).ToList(); + pathAndQuery.ShouldAllBe(pathQuery => pathWithQueryString.Contains(pathQuery)); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + public void ShouldApplyGlobalLoadBalancerOptions_ForAllDynamicRoutes() + { + var ports = PortFinder.GetPorts(5); + var serviceName = ServiceName(); + var serviceUrls = ports.Select(DownstreamUrl).ToArray(); + var configuration = GivenDynamicRouting(new() + { + { serviceName, serviceUrls }, + }); + GivenMultipleServiceInstancesAreRunning(serviceUrls); + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunning(WithDiscovery); + WhenIGetUrlOnTheApiGatewayConcurrently($"/{serviceName}/", 50); + ThenAllServicesShouldHaveBeenCalledTimes(50); + ThenAllServicesCalledRealisticAmountOfTimes(9, 11); // soft assertion + ThenServicesShouldHaveBeenCalledTimes(10, 10, 10, 10, 10); // distribution by RoundRobin algorithm, aka strict assertion + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + public void ShouldApplyGlobalGroupLoadBalancerOptions_ForDynamicRoutes_WhenRouteOptsHasAKey() + { + // 1st route + var ports1 = PortFinder.GetPorts(2); + var route1 = GivenLbRoute("route1"); + route1.LoadBalancerOptions = null; // 1st route is not balanced + route1.Key = null; // 1st route is not in the global group + route1.Metadata = new Dictionary() + { + { route1.ServiceName, ports1.Select(DownstreamUrl).Csv() }, + }; + + // 2nd route + var ports2 = PortFinder.GetPorts(2); + var route2 = GivenLbRoute("route2"); + route2.LoadBalancerOptions = null; // 2nd route opts will be applied from global ones + route2.Key = "R2"; // 2nd route is in the group + route2.Metadata = new Dictionary() + { + { route2.ServiceName, ports2.Select(DownstreamUrl).Csv() }, + }; + + // 3rd route + var ports3 = PortFinder.GetPorts(2); + var route3 = GivenLbRoute("noLoadBalancing", loadBalancer: nameof(NoLoadBalancer)); + route3.Metadata = new Dictionary() + { + { route3.ServiceName, ports3.Select(DownstreamUrl).Csv() }, + }; + + var configuration = GivenDynamicRouting(new(), route1, route2, route3); + configuration.GlobalConfiguration.LoadBalancerOptions = new() + { + RouteKeys = ["R2"], + Type = nameof(RoundRobin), + }; + + var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); + GivenMultipleServiceInstancesAreRunning(downstreamUrls); + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunning(WithDiscovery); + + WhenIGetUrlOnTheApiGatewayConcurrently("/route1/", 2); + WhenIGetUrlOnTheApiGatewayConcurrently("/route2/", 4); + WhenIGetUrlOnTheApiGatewayConcurrently("/noLoadBalancing/", 5); + ThenServicesShouldHaveBeenCalledTimes(2, 0, 2, 2, 5, 0); // main assertion, explanation is below + ThenServiceShouldHaveBeenCalledTimes(0, 2); // NoLoadBalancer for 2 + ThenServiceShouldHaveBeenCalledTimes(1, 0); // NoLoadBalancer for 2 + ThenServiceShouldHaveBeenCalledTimes(2, 2); // RoundRobin for 4 + ThenServiceShouldHaveBeenCalledTimes(3, 2); // RoundRobin for 4 + ThenServiceShouldHaveBeenCalledTimes(4, 5); // NoLoadBalancer for 5 + ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 5 + } + + private FileConfiguration GivenDynamicRouting(Dictionary> services, params FileDynamicRoute[] routes) + { + var config = new FileConfiguration() + { + DynamicRoutes = new(routes), + GlobalConfiguration = new() + { + DownstreamScheme = Uri.UriSchemeHttp, + ServiceDiscoveryProvider = new() + { + Type = nameof(DynamicRoutingDiscoveryProvider), + Host = "doesn't matter for this provider", // it should not be empty due to DownstreamRouteProviderFactory.Get + Port = 1, // see DownstreamRouteProviderFactory.IsServiceDiscovery + }, + LoadBalancerOptions = new(nameof(RoundRobin)), + }, + }; + config.GlobalConfiguration.Metadata = services.ToDictionary(x => x.Key, x => x.Value.Csv()); + return config; + } + + private FileDynamicRoute GivenLbRoute(string serviceName, string serviceNamespace = null, string loadBalancer = null) + { + var route = new FileDynamicRoute() + { + ServiceName = serviceName, + ServiceNamespace = serviceNamespace ?? ServiceNamespace(), + LoadBalancerOptions = new(loadBalancer ?? nameof(NoLoadBalancer)), + }; + return route; + } + + private static readonly ServiceDiscoveryFinderDelegate DynamicRoutingDiscoveryFinder = (provider, config, route) + => new DynamicRoutingDiscoveryProvider(provider, config, route); + private static void WithDiscovery(IServiceCollection services) => services + .AddSingleton(DynamicRoutingDiscoveryFinder) + .AddOcelot(); + + protected override string ServiceNamespace() => nameof(DynamicRoutingTests); +} + +public class DynamicRoutingDiscoveryProvider : IServiceDiscoveryProvider +{ + private readonly IServiceProvider _serviceProvider; + private readonly ServiceProviderConfiguration _config; + private readonly DownstreamRoute _downstreamRoute; + + public DynamicRoutingDiscoveryProvider(IServiceProvider serviceProvider, ServiceProviderConfiguration config, DownstreamRoute downstreamRoute) + { + _serviceProvider = serviceProvider; + _config = config; + _downstreamRoute = downstreamRoute; + } + + public Task> GetAsync() + { + if (!_downstreamRoute.MetadataOptions.Metadata.TryGetValue(_downstreamRoute.ServiceName, out var data) + || data.IsEmpty()) + return Task.FromResult>(new()); + + var urls = _downstreamRoute + .GetMetadata(_downstreamRoute.ServiceName) + .Select(x => new Uri(x)) + .ToList(); + var services = urls + .Select(url => new Service( + name: _downstreamRoute.ServiceName, + hostAndPort: new(url.Host, url.Port, url.Scheme.IfEmpty(_downstreamRoute.DownstreamScheme)), + id: $"{_downstreamRoute.ServiceNamespace}.{_downstreamRoute.ServiceName}", + version: DateTime.UtcNow.ToString("O"), + tags: Enumerable.Empty())) + .ToList(); + return Task.FromResult(services); + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs index 0cc1761a4..64df78b2c 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; using Ocelot.Provider.Eureka; using Steeltoe.Common.Discovery; using System.Runtime.CompilerServices; diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs index f91254246..d620183f2 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs @@ -9,7 +9,7 @@ using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; @@ -40,21 +40,20 @@ public KubernetesServiceDiscoveryTests() [Theory] [InlineData(nameof(Kube))] + /* [InlineData(nameof(PollKube))] TODO Fails now. Bug 2304? -> https://github.com/ThreeMammals/Ocelot/issues/2304 */ [InlineData(nameof(WatchKube))] public void ShouldReturnServicesFromK8s(string discoveryType) { - const string namespaces = nameof(KubernetesServiceDiscoveryTests); - const string serviceName = nameof(ShouldReturnServicesFromK8s); var servicePort = PortFinder.GetRandomPort(); var downstreamUrl = LoopbackLocalhostUrl(servicePort); var downstream = new Uri(downstreamUrl); var subsetV1 = GivenSubsetAddress(downstream); var endpoints = GivenEndpoints(subsetV1); - var route = GivenRouteWithServiceName(namespaces); - var configuration = GivenKubeConfiguration(namespaces, route, discoveryType); - var downstreamResponse = serviceName; + var route = GivenRouteWithServiceName(ServiceName()); + var configuration = GivenKubeConfiguration(route, discoveryType); + string serviceName = ServiceName(), downstreamResponse = serviceName; this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, downstreamResponse)) - .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName, namespaces)) + .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning(WithKubernetes)) .When(_ => GivenWatchReceivedEvent()) @@ -73,7 +72,6 @@ public void ShouldReturnServicesFromK8s(string discoveryType) public void ShouldReturnServicesByPortNameAsDownstreamScheme(string downstreamScheme, HttpStatusCode statusCode) { const string serviceName = "example-web"; - const string namespaces = "default"; var servicePort = PortFinder.GetRandomPort(); var downstreamUrl = LoopbackLocalhostUrl(servicePort); var downstream = new Uri(downstreamUrl); @@ -87,15 +85,15 @@ public void ShouldReturnServicesByPortNameAsDownstreamScheme(string downstreamSc Port = 443, }); var endpoints = GivenEndpoints(subsetV1); - var route = GivenRouteWithServiceName(namespaces); + var route = GivenRouteWithServiceName(); route.DownstreamPathTemplate = "/{url}"; route.DownstreamScheme = downstreamScheme; // !!! Warning !!! Select port by name as scheme route.UpstreamPathTemplate = "/api/example/{url}"; route.ServiceName = serviceName; // "example-web" - var configuration = GivenKubeConfiguration(namespaces, route, nameof(Kube)); + var configuration = GivenKubeConfiguration(route, nameof(Kube)); this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, nameof(ShouldReturnServicesByPortNameAsDownstreamScheme))) - .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName, namespaces)) + .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning(WithKubernetes)) .When(_ => WhenIGetUrlOnTheApiGateway("/api/example/1")) @@ -127,7 +125,7 @@ public void ShouldHighlyLoadOnStableKubeProvider_WithRoundRobinLoadBalancing(int return; const int ZeroGeneration = 0; - var (endpoints, servicePorts) = ArrangeHighLoadOnKubeProviderAndRoundRobinBalancer(totalServices); + var (endpoints, servicePorts) = GivenServiceDiscoveryAndLoadBalancing(totalServices); GivenThereIsAFakeKubernetesProvider(endpoints); // stable, services will not be removed from the list HighlyLoadOnKubeProviderAndRoundRobinBalancer(totalRequests, ZeroGeneration); @@ -151,7 +149,7 @@ public void ShouldHighlyLoadOnUnstableKubeProvider_WithRoundRobinLoadBalancing(i return; int failPerThreads = (totalRequests / k8sGeneration) - 1; // k8sGeneration means number of offline services - var (endpoints, servicePorts) = ArrangeHighLoadOnKubeProviderAndRoundRobinBalancer(totalServices); + var (endpoints, servicePorts) = GivenServiceDiscoveryAndLoadBalancing(totalServices); GivenThereIsAFakeKubernetesProvider(endpoints, false, k8sGeneration, failPerThreads); // false means unstable, k8sGeneration services will be removed from the list HighlyLoadOnKubeProviderAndRoundRobinBalancer(totalRequests, k8sGeneration); @@ -166,18 +164,16 @@ public void ShouldHighlyLoadOnUnstableKubeProvider_WithRoundRobinLoadBalancing(i [Trait("Feat", "2256")] public void ShouldReturnServicesFromK8s_AddKubernetesWithNullConfigureOptions(string discoveryType) { - const string namespaces = nameof(KubernetesServiceDiscoveryTests); - const string serviceName = nameof(ShouldReturnServicesFromK8s_AddKubernetesWithNullConfigureOptions); var servicePort = PortFinder.GetRandomPort(); var downstreamUrl = LoopbackLocalhostUrl(servicePort); var downstream = new Uri(downstreamUrl); var subsetV1 = GivenSubsetAddress(downstream); var endpoints = GivenEndpoints(subsetV1); - var route = GivenRouteWithServiceName(namespaces); - var configuration = GivenKubeConfiguration(namespaces, route, discoveryType, "txpc696iUhbVoudg164r93CxDTrKRVWG"); - var downstreamResponse = serviceName; + var route = GivenRouteWithServiceName(); + var configuration = GivenKubeConfiguration(route, discoveryType, "txpc696iUhbVoudg164r93CxDTrKRVWG"); + string serviceName = ServiceName(), downstreamResponse = serviceName; this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, downstreamResponse)) - .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName, namespaces)) + .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning(AddKubernetesWithNullConfigureOptions)) .When(_ => GivenWatchReceivedEvent()) @@ -194,25 +190,21 @@ public void ShouldReturnServicesFromK8s_AddKubernetesWithNullConfigureOptions(st [Trait("PR", "2174")] // https://github.com/ThreeMammals/Ocelot/pull/2174 public void ShouldReturnServicesFromK8s_OneWatchRequestUpdatesServicesInfo() { - const string namespaces = nameof(KubernetesServiceDiscoveryTests); - const string serviceName = nameof(ShouldReturnServicesFromK8s_OneWatchRequestUpdatesServicesInfo); (EndpointsV1 endpoints, string downstreamUrl) = GetServiceInstance(); (EndpointsV1 updatedEndpoints, string updateDownstreamUrl) = GetServiceInstance(); - ResourceEventV1[] events = [ new() { EventType = ResourceEventType.Added, Resource = endpoints }, new() { EventType = ResourceEventType.Modified, Resource = updatedEndpoints } ]; + var route = GivenRouteWithServiceName(); + var configuration = GivenKubeConfiguration(route, nameof(WatchKube)); - var route = GivenRouteWithServiceName(namespaces); - var configuration = GivenKubeConfiguration(namespaces, route, nameof(WatchKube)); - - var downstreamResponse = serviceName; + string serviceName = ServiceName(), downstreamResponse = serviceName; var updatedDownstreamResponse = "updated_content" + serviceName; this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, downstreamResponse)) .Given(x => GivenServiceInstanceIsRunning(updateDownstreamUrl, updatedDownstreamResponse)) - .And(x => x.GivenThereIsAFakeKubernetesProvider(events, serviceName, namespaces)) + .And(x => x.GivenThereIsAFakeKubernetesProvider(events, serviceName)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning(WithKubernetes)) .When(_ => GivenWatchReceivedEvent()) @@ -241,14 +233,53 @@ public void ShouldReturnServicesFromK8s_OneWatchRequestUpdatesServicesInfo() } } + [Theory] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + [InlineData(nameof(Kube))] + /* [InlineData(nameof(PollKube))] // Bug 2304 -> https://github.com/ThreeMammals/Ocelot/issues/2304 */ + [InlineData(nameof(WatchKube))] + public void ShouldApplyGlobalLoadBalancerOptions_ForAllDynamicRoutes(string discoveryType) + { + static void ConfigureDynamicRouting(FileConfiguration configuration) + { + configuration.GlobalConfiguration.LoadBalancerOptions = new(nameof(RoundRobin)); + configuration.GlobalConfiguration.DownstreamScheme = Uri.UriSchemeHttp; + configuration.Routes = []; // dynamic routing + configuration.DynamicRoutes = []; // no dynamic routes, for ALL dynamic routes + } + var (endpoints, servicePorts) = GivenServiceDiscoveryAndLoadBalancing( + 5, discoveryType, nameof(RoundRobin), + ConfigureDynamicRouting, + WithKubernetesAndFakeKubeServiceCreator); + GivenThereIsAFakeKubernetesProvider(endpoints); + if (discoveryType == nameof(WatchKube)) + GivenWatchReceivedEvent(); + + var upstreamPath = $"/{ServiceNamespace()}.{ServiceName()}/"; + WhenIGetUrlOnTheApiGatewayConcurrently(upstreamPath, 50); + + _k8sCounter.ShouldBe(discoveryType == nameof(WatchKube) ? 1 : 50); + _k8sServiceGeneration.ShouldBe(0); + ThenAllStatusCodesShouldBe(HttpStatusCode.OK); + ThenAllServicesShouldHaveBeenCalledTimes(50); + ThenAllServicesCalledRealisticAmountOfTimes(9, 11); // soft assertion + ThenServicesShouldHaveBeenCalledTimes(10, 10, 10, 10, 10); // distribution by RoundRobin algorithm, aka strict assertion + } + private void AddKubernetesWithNullConfigureOptions(IServiceCollection services) => services.AddOcelot().AddKubernetes(configureOptions: null); - private (EndpointsV1 Endpoints, int[] ServicePorts) ArrangeHighLoadOnKubeProviderAndRoundRobinBalancer( + private (EndpointsV1 Endpoints, int[] ServicePorts) GivenServiceDiscoveryAndLoadBalancing( int totalServices, - [CallerMemberName] string serviceName = nameof(ArrangeHighLoadOnKubeProviderAndRoundRobinBalancer)) + string discoveryType = nameof(Kube), + string loadBalancerType = nameof(RoundRobinAnalyzer), + Action configure = null, + Action services = null, + [CallerMemberName] string serviceName = null) { - const string namespaces = nameof(KubernetesServiceDiscoveryTests); + serviceName ??= ServiceName(); var servicePorts = PortFinder.GetPorts(totalServices); var downstreamUrls = servicePorts .Select(port => LoopbackLocalhostUrl(port, Array.IndexOf(servicePorts, port))) @@ -261,11 +292,12 @@ private void AddKubernetesWithNullConfigureOptions(IServiceCollection services) var subset = new EndpointSubsetV1(); downstreams.ForEach(ds => GivenSubsetAddress(ds, subset)); var endpoints = GivenEndpoints(subset, serviceName); // totalServices service instances with different ports - var route = GivenRouteWithServiceName(namespaces, serviceName, nameof(RoundRobinAnalyzer)); // !!! - var configuration = GivenKubeConfiguration(namespaces, route, nameof(Kube)); + var route = GivenRouteWithServiceName(serviceName, loadBalancerType); // !!! + var configuration = GivenKubeConfiguration(route, discoveryType); + configure?.Invoke(configuration); GivenMultipleServiceInstancesAreRunning(downstreamUrls, downstreamResponses); GivenThereIsAConfiguration(configuration); - GivenOcelotIsRunning(WithKubernetesAndRoundRobin); + GivenOcelotIsRunning(services ?? WithKubernetesAndRoundRobin); return (endpoints, servicePorts); } @@ -284,15 +316,8 @@ private void HighlyLoadOnKubeProviderAndRoundRobinBalancer(int totalRequests, in _roundRobinAnalyzer.HasManyServiceGenerations(k8sGenerationNo).ShouldBeTrue(); } - private void ThenTheTokenIs(string token) - { - _receivedToken.ShouldBe(token); - } - - private void ThenK8sShouldBeCalledExactly(int totalRequests) - { - _k8sCounter.ShouldBe(totalRequests); - } + private void ThenTheTokenIs(string token) => _receivedToken.ShouldBe(token); + private void ThenK8sShouldBeCalledExactly(int totalRequests) => _k8sCounter.ShouldBe(totalRequests); private EndpointsV1 GivenEndpoints(EndpointSubsetV1 subset, [CallerMemberName] string serviceName = "") { @@ -303,7 +328,7 @@ private EndpointsV1 GivenEndpoints(EndpointSubsetV1 subset, [CallerMemberName] s Metadata = new() { Name = serviceName, - Namespace = nameof(KubernetesServiceDiscoveryTests), + Namespace = ServiceNamespace(), }, }; e.Subsets.Add(subset); @@ -326,8 +351,7 @@ private static EndpointSubsetV1 GivenSubsetAddress(Uri downstream, EndpointSubse return subset; } - private FileRoute GivenRouteWithServiceName(string serviceNamespace, - [CallerMemberName] string serviceName = null, + private FileRoute GivenRouteWithServiceName([CallerMemberName] string serviceName = null, string loadBalancerType = nameof(LeastConnection)) => new() { DownstreamPathTemplate = "/", @@ -335,11 +359,11 @@ private FileRoute GivenRouteWithServiceName(string serviceNamespace, UpstreamPathTemplate = "/", UpstreamHttpMethod = [HttpMethods.Get], ServiceName = serviceName, // !!! - ServiceNamespace = serviceNamespace, + ServiceNamespace = ServiceNamespace(), LoadBalancerOptions = new() { Type = loadBalancerType }, }; - private FileConfiguration GivenKubeConfiguration(string serviceNamespace, FileRoute route, string type, string token = null) + private FileConfiguration GivenKubeConfiguration(FileRoute route, string type, string token = null) { var u = new Uri(_kubernetesUrl); var configuration = GivenConfiguration(route); @@ -350,20 +374,22 @@ private FileConfiguration GivenKubeConfiguration(string serviceNamespace, FileRo Port = u.Port, Type = type, PollingInterval = 0, - Namespace = serviceNamespace, + Namespace = ServiceNamespace(), Token = token ?? "Test", }; return configuration; } private void GivenThereIsAFakeKubernetesProvider(EndpointsV1 endpoints, - [CallerMemberName] string serviceName = nameof(KubernetesServiceDiscoveryTests), string namespaces = nameof(KubernetesServiceDiscoveryTests)) - => GivenThereIsAFakeKubernetesProvider(endpoints, true, 0, 0, serviceName, namespaces); + [CallerMemberName] string serviceName = nameof(KubernetesServiceDiscoveryTests)) + => GivenThereIsAFakeKubernetesProvider(endpoints, true, 0, 0, serviceName, ServiceNamespace()); private void GivenThereIsAFakeKubernetesProvider(EndpointsV1 endpoints, bool isStable, int offlineServicesNo, int offlinePerThreads, - [CallerMemberName] string serviceName = nameof(KubernetesServiceDiscoveryTests), string namespaces = nameof(KubernetesServiceDiscoveryTests)) + [CallerMemberName] string serviceName = null, string namespaces = null) { _k8sCounter = 0; + serviceName ??= ServiceName(); + namespaces ??= ServiceNamespace(); handler.GivenThereIsAServiceRunningOn(_kubernetesUrl, async context => { await Task.Delay(Random.Shared.Next(1, 10)); // emulate integration delay up to 10 milliseconds @@ -409,15 +435,14 @@ await GivenHandleWatchRequest(context, } private void GivenThereIsAFakeKubernetesProvider(ResourceEventV1[] events, - [CallerMemberName] string serviceName = nameof(KubernetesServiceDiscoveryTests), - string namespaces = nameof(KubernetesServiceDiscoveryTests)) + [CallerMemberName] string serviceName = nameof(KubernetesServiceDiscoveryTests)) { _k8sCounter = 0; + var namespaces = ServiceNamespace(); handler.GivenThereIsAServiceRunningOn(_kubernetesUrl, (c) => GivenHandleWatchRequest(c, events, namespaces, serviceName)); } private void GivenWatchReceivedEvent() => _k8sWatchResetEvent.Set(); - private static Task GivenDelay(int milliseconds) => Task.Delay(TimeSpan.FromMilliseconds(milliseconds)); private async Task GivenHandleWatchRequest(HttpContext context, @@ -462,6 +487,8 @@ private IOcelotBuilder AddKubernetes(IServiceCollection services) => services .AddOcelot().AddKubernetes(_kubeClientOptionsConfigure); private void WithKubernetes(IServiceCollection services) => AddKubernetes(services); + private void WithKubernetesAndFakeKubeServiceCreator(IServiceCollection services) => AddKubernetes(services) + .Services.RemoveAll().AddSingleton(); private void WithKubernetesAndRoundRobin(IServiceCollection services) => AddKubernetes(services) .AddCustomLoadBalancer(GetRoundRobinAnalyzer) .Services.RemoveAll().AddSingleton(); @@ -477,6 +504,9 @@ private RoundRobinAnalyzer GetRoundRobinAnalyzer(DownstreamRoute route, IService return _roundRobinAnalyzer ??= new RoundRobinAnalyzerCreator().Create(route, provider)?.Data as RoundRobinAnalyzer; //??= new RoundRobinAnalyzer(provider.GetAsync, route.ServiceName); } } + + protected override string ServiceName([CallerMemberName] string serviceName = null) => serviceName ?? nameof(KubernetesServiceDiscoveryTests); + protected override string ServiceNamespace() => nameof(KubernetesServiceDiscoveryTests); } internal class FakeKubeServiceCreator : KubeServiceCreator diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs index 66267c2e8..be1f3a6a6 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs @@ -26,7 +26,7 @@ public void Should_fix_issue_555() DownstreamPathTemplate = "/{everything}", DownstreamScheme = "http", UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], ServiceName = "OcelotServiceApplication/OcelotApplicationService", }, }, @@ -63,7 +63,7 @@ public void Should_support_service_fabric_naming_and_dns_service_stateless_and_g DownstreamPathTemplate = "/api/values", DownstreamScheme = "http", UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], ServiceName = "OcelotServiceApplication/OcelotApplicationService", }, }, @@ -100,7 +100,7 @@ public void Should_support_service_fabric_naming_and_dns_service_statefull_and_a DownstreamPathTemplate = "/api/values", DownstreamScheme = "http", UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], ServiceName = "OcelotServiceApplication/OcelotApplicationService", }, }, diff --git a/test/Ocelot.AcceptanceTests/WebSockets/ClientWebSocketTests.cs b/test/Ocelot.AcceptanceTests/WebSockets/ClientWebSocketTests.cs index 6ecca45cd..13f5c4520 100644 --- a/test/Ocelot.AcceptanceTests/WebSockets/ClientWebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/WebSockets/ClientWebSocketTests.cs @@ -62,7 +62,7 @@ public async Task Http20CLient_DirectConnection_ShouldConnect() using var handler = new HttpClientHandler { // ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true, - PreAuthenticate = true, + PreAuthenticate = false, Credentials = new NetworkCredential("tom@threemammals.com", "password"), }; using var invoker = new HttpMessageInvoker(handler); diff --git a/test/Ocelot.AcceptanceTests/WebSockets/WebSocketsFactoryTests.cs b/test/Ocelot.AcceptanceTests/WebSockets/WebSocketsFactoryTests.cs index 4ec69e870..d0a1af222 100644 --- a/test/Ocelot.AcceptanceTests/WebSockets/WebSocketsFactoryTests.cs +++ b/test/Ocelot.AcceptanceTests/WebSockets/WebSocketsFactoryTests.cs @@ -1,5 +1,5 @@ using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; namespace Ocelot.AcceptanceTests.WebSockets; @@ -32,7 +32,7 @@ public void ShouldProxyWebsocketInputToDownstreamServiceAndUseLoadBalancer() int port1 = PortFinder.GetRandomPort(); int port2 = PortFinder.GetRandomPort(); var route = GivenRoute("/ws", port1, port2); - route.LoadBalancerOptions.Type = nameof(RoundRobin); + route.LoadBalancerOptions = new(nameof(RoundRobin)); var configuration = GivenConfiguration(route); int ocelotPort = PortFinder.GetRandomPort(); this.Given(_ => GivenThereIsAConfiguration(configuration)) diff --git a/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs b/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs index fec5d9e5d..b154a6929 100644 --- a/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs +++ b/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs @@ -44,7 +44,7 @@ public void SetUp() }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; diff --git a/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs b/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs index e5c15900e..ce024627b 100644 --- a/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs +++ b/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs @@ -52,8 +52,7 @@ public void SetUp() }, }; httpContext.Request.Headers.Append("Host", "most"); - httpContext.Items.SetIInternalConfiguration(new InternalConfiguration(new List(), null, null, null, null, null, null, null, null, null)); - + httpContext.Items.SetIInternalConfiguration(new InternalConfiguration()); _httpContext = httpContext; } diff --git a/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs b/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs index b8b53ff1b..242eaed20 100644 --- a/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs +++ b/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs @@ -148,7 +148,7 @@ private void OcelotFactory(LogLevel minLogLevel) }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj index e6d3c0bf5..3e192c4e0 100644 --- a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj @@ -21,7 +21,7 @@ - + diff --git a/test/Ocelot.Benchmarks/SerilogBenchmarks.cs b/test/Ocelot.Benchmarks/SerilogBenchmarks.cs index cd2adcb98..1c9d89a1c 100644 --- a/test/Ocelot.Benchmarks/SerilogBenchmarks.cs +++ b/test/Ocelot.Benchmarks/SerilogBenchmarks.cs @@ -179,7 +179,7 @@ private void OcelotFactory(LogLevel minLogLevel) }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, + UpstreamHttpMethod = ["Get"], }, }, }; diff --git a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj index 935de5111..38006096c 100644 --- a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +++ b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj @@ -44,7 +44,7 @@ - + all diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs index 9f0384113..75fe64b70 100644 --- a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs @@ -66,7 +66,7 @@ public void Should_call_next_middleware_if_route_is_not_authenticated() { // Arrange GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) + .WithUpstreamHttpMethod([HttpMethods.Get]) .Build()); // Act @@ -81,7 +81,7 @@ public void Should_call_next_middleware_if_route_is_using_options_method() { // Arrange GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() - .WithUpstreamHttpMethod(new() { HttpMethods.Options }) + .WithUpstreamHttpMethod([HttpMethods.Options]) .WithIsAuthenticated(true) .Build()); GivenTheRequestIsUsingMethod(HttpMethods.Options); diff --git a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs index 3cbf3256f..88befbb8b 100644 --- a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs @@ -42,7 +42,7 @@ public async Task Should_call_authorization_service() new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().Build()) .WithIsAuthorized(true) - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) + .WithUpstreamHttpMethod([HttpMethods.Get]) .Build()); GivenTheAuthServiceReturns(new OkResponse(true)); diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index 7ce107907..b5997c695 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -4,7 +4,6 @@ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; @@ -110,19 +109,17 @@ private void GivenResponseIsNotCached(HttpResponseMessage responseMessage) private void GivenTheDownstreamRouteIs() { - var route = new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken", null, false)) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) + var downRoute = new DownstreamRouteBuilder() + .WithIsCached(true) + .WithCacheOptions(new CacheOptions(100, "kanken", null, false)) + .WithUpstreamHttpMethod([ "Get" ]) .Build(); - + var route = new Route(downRoute) + { + UpstreamHttpMethod = [HttpMethod.Get], + }; var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); - _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); - _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } diff --git a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs index d52e8876e..1b654906b 100644 --- a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs @@ -35,19 +35,21 @@ public ClaimsToClaimsMiddlewareTests() public async Task Should_call_claims_to_request_correctly() { // Arrange + var route = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithClaimsToClaims(new() + { + new("sub", "UserType", "|", 0), + }) + .WithUpstreamHttpMethod([HttpMethods.Get]) + .Build(); var downstreamRoute = new DownstreamRouteHolder( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("any old string") - .WithClaimsToClaims(new() - { - new("sub", "UserType", "|", 0), - }) - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) - .Build()) - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) - .Build()); + new Route() + { + DownstreamRoute = [route], + UpstreamHttpMethod = [HttpMethod.Get], + }); GivenTheDownStreamRouteIs(downstreamRoute); GivenTheAddClaimsToRequestReturns(); diff --git a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs index a915b562f..077eb7c16 100644 --- a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs @@ -81,10 +81,10 @@ public void Should_create_aggregates() }; var routes = new List { - new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().WithKey("key1").Build()).Build(), - new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().WithKey("key2").Build()).Build(), - new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().WithKey("key3").Build()).Build(), - new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().WithKey("key4").Build()).Build(), + new(new DownstreamRouteBuilder().WithKey("key1").Build()), + new(new DownstreamRouteBuilder().WithKey("key2").Build()), + new(new DownstreamRouteBuilder().WithKey("key3").Build()), + new(new DownstreamRouteBuilder().WithKey("key4").Build()), }; GivenThe(fileConfig); diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs index df88dd19b..23a0424b0 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs @@ -16,7 +16,9 @@ public class ConfigurationCreatorTests : UnitTest private readonly Mock _hhoCreator; private readonly Mock _lboCreator; private readonly Mock _vCreator; - private readonly Mock _versionPolicyCreator; + private readonly Mock _vpCreator; + private readonly Mock _mdCreator; + private readonly Mock _rlCreator; private FileConfiguration _fileConfig; private List _routes; private ServiceProviderConfiguration _spc; @@ -29,11 +31,13 @@ public class ConfigurationCreatorTests : UnitTest public ConfigurationCreatorTests() { _vCreator = new Mock(); - _versionPolicyCreator = new Mock(); + _vpCreator = new Mock(); _lboCreator = new Mock(); _hhoCreator = new Mock(); _qosCreator = new Mock(); _spcCreator = new Mock(); + _mdCreator = new Mock(); + _rlCreator = new Mock(); _serviceCollection = new ServiceCollection(); } @@ -49,7 +53,7 @@ public void Should_build_configuration_with_no_admin_path() // Assert ThenTheDepdenciesAreCalledCorrectly(); ThenThePropertiesAreSetCorrectly(); - ThenTheAdminPathIsNull(); + _result.AdministrationPath.ShouldBeNull(); } [Fact] @@ -68,13 +72,24 @@ public void Should_build_configuration_with_admin_path() ThenTheAdminPathIsSet(); } - private void ThenTheAdminPathIsNull() + [Fact] + public void Configuration_GlobalConfiguration_SoftNullGuard() { - _result.AdministrationPath.ShouldBeNull(); + // Arrange + GivenTheDependenciesAreSetUp(); + _fileConfig.GlobalConfiguration = null; + + // Act + WhenICreate(); + + // Assert + ThenTheDepdenciesAreCalledCorrectly(); + ThenThePropertiesAreSetCorrectly(); } private void ThenThePropertiesAreSetCorrectly() { + _fileConfig.GlobalConfiguration ??= new(); _result.ShouldNotBeNull(); _result.ServiceProviderConfiguration.ShouldBe(_spc); _result.LoadBalancerOptions.ShouldBe(_lbo); @@ -92,10 +107,15 @@ private void ThenTheAdminPathIsSet() private void ThenTheDepdenciesAreCalledCorrectly() { - _spcCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration), Times.Once); - _lboCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration.LoadBalancerOptions), Times.Once); - _qosCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration.QoSOptions), Times.Once); - _hhoCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration.HttpHandlerOptions), Times.Once); + _spcCreator.Verify(x => x.Create(It.IsAny()), Times.Once); + _lboCreator.Verify(x => x.Create(It.IsAny()), Times.Once); + _qosCreator.Verify(x => x.Create(It.IsAny()), Times.Once); + _hhoCreator.Verify(x => x.Create(It.IsAny()), Times.Once); + _vCreator.Verify(x => x.Create(It.IsAny()), Times.Once); + _vpCreator.Verify(x => x.Create(It.IsAny()), Times.Once); + _mdCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Once); + _vCreator.Verify(x => x.Create(It.IsAny()), Times.Once); + _rlCreator.Verify(x => x.Create(It.IsAny()), Times.Once); } private void GivenTheAdminPath() @@ -112,7 +132,7 @@ private void GivenTheDependenciesAreSetUp() }; _routes = new List(); _spc = new ServiceProviderConfiguration(string.Empty, string.Empty, string.Empty, 1, string.Empty, string.Empty, 1); - _lbo = new LoadBalancerOptionsBuilder().Build(); + _lbo = new(); _qoso = new QoSOptionsBuilder().Build(); _hho = new HttpHandlerOptionsBuilder().Build(); @@ -125,7 +145,7 @@ private void GivenTheDependenciesAreSetUp() private void WhenICreate() { var serviceProvider = _serviceCollection.BuildServiceProvider(true); - _creator = new ConfigurationCreator(_spcCreator.Object, _qosCreator.Object, _hhoCreator.Object, serviceProvider, _lboCreator.Object, _vCreator.Object, _versionPolicyCreator.Object); + _creator = new ConfigurationCreator(_spcCreator.Object, _qosCreator.Object, _hhoCreator.Object, serviceProvider, _lboCreator.Object, _vCreator.Object, _vpCreator.Object, _mdCreator.Object, _rlCreator.Object); _result = _creator.Create(_fileConfig, _routes); } } diff --git a/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs index 65deab007..feb47050d 100644 --- a/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs @@ -62,12 +62,9 @@ public void Should_overwrite_global_metadata() private static FileGlobalConfiguration GivenSomeMetadataInGlobalConfiguration() => new() { - MetadataOptions = new() + Metadata = new Dictionary { - Metadata = new Dictionary - { - ["foo"] = "bar", - }, + ["foo"] = "bar", }, }; diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicRoutesCreatorTests.cs similarity index 61% rename from test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs rename to test/Ocelot.UnitTests/Configuration/DynamicRoutesCreatorTests.cs index 035c07079..e0216b1b4 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicRoutesCreatorTests.cs @@ -1,192 +1,197 @@ using Ocelot.Configuration; -using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; - -namespace Ocelot.UnitTests.Configuration; - -public class DynamicsCreatorTests : UnitTest -{ - private readonly DynamicsCreator _creator; - private readonly Mock _rloCreator; - private readonly Mock _versionCreator; - private readonly Mock _versionPolicyCreator; - private readonly Mock _metadataCreator; - private IReadOnlyList _result; - private FileConfiguration _fileConfig; - private RateLimitOptions _rlo1; - private RateLimitOptions _rlo2; - private Version _version; + +namespace Ocelot.UnitTests.Configuration; + +public class DynamicRoutesCreatorTests : UnitTest +{ + private readonly DynamicRoutesCreator _creator; + private readonly Mock _lbKeyCreator = new(); + private readonly Mock _lboCreator = new(); + private readonly Mock _qosCreator = new(); + private readonly Mock _rloCreator = new(); + private readonly Mock _versionCreator = new(); + private readonly Mock _versionPolicyCreator = new(); + private readonly Mock _metadataCreator = new(); + private IReadOnlyList _result; + private FileConfiguration _fileConfig; + private RateLimitOptions[] _rlo; + private Version _version; private HttpVersionPolicy _versionPolicy; private Dictionary _expectedMetadata; - - public DynamicsCreatorTests() - { - _versionCreator = new Mock(); - _versionPolicyCreator = new Mock(); - _metadataCreator = new Mock(); - _rloCreator = new Mock(); - _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object, _metadataCreator.Object); - } - - [Fact] - public void Should_return_nothing() - { - // Arrange - _fileConfig = new FileConfiguration(); - - // Act - _result = _creator.Create(_fileConfig); - - // Assert - _result.Count.ShouldBe(0); - - // Assert: then the RloCreator is not called - _rloCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); - - // Assert: then the metadata creator is not called + + public DynamicRoutesCreatorTests() + { + _creator = new DynamicRoutesCreator( + _lbKeyCreator.Object, + _lboCreator.Object, + _metadataCreator.Object, _qosCreator.Object, + _rloCreator.Object, + _versionCreator.Object, + _versionPolicyCreator.Object); + } + + [Fact] + public void Should_return_nothing() + { + // Arrange + _fileConfig = new FileConfiguration(); + + // Act + _result = _creator.Create(_fileConfig); + + // Assert + _result.Count.ShouldBe(0); + + // Assert: then the RloCreator is not called + _lbKeyCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); + _lboCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); + _rloCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); + + // Assert: then the metadata creator is not called _metadataCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Never); - } - - [Fact] - public void Should_return_routes() - { - // Arrange - _fileConfig = new FileConfiguration - { - DynamicRoutes = new() - { - GivenDynamicRoute("1", false, "1.1", "foo", "bar"), - GivenDynamicRoute("2", true, "2.0", "foo", "baz"), - }, - }; - GivenTheRloCreatorReturns(); - GivenTheVersionCreatorReturns(); - GivenTheVersionPolicyCreatorReturns(); - GivenTheMetadataCreatorReturns(); - - // Act - _result = _creator.Create(_fileConfig); - - // Assert - ThenTheRoutesAreReturned(); - ThenTheRloCreatorIsCalledCorrectly(); - ThenTheVersionCreatorIsCalledCorrectly(); + } + + [Fact] + public void Should_return_routes() + { + // Arrange + _fileConfig = new FileConfiguration + { + DynamicRoutes = new() + { + GivenDynamicRoute("1", false, "1.1", "foo", "bar"), + GivenDynamicRoute("2", true, "2.0", "foo", "baz"), + }, + }; + GivenTheRloCreatorReturns(); + GivenTheVersionCreatorReturns(); + GivenTheVersionPolicyCreatorReturns(); + GivenTheMetadataCreatorReturns(); + + // Act + _result = _creator.Create(_fileConfig); + + // Assert + ThenTheRoutesAreReturned(); + ThenTheBasicCreatorsAreCalledCorrectly(); + ThenTheVersionCreatorIsCalledCorrectly(); ThenTheMetadataCreatorIsCalledCorrectly(); - } - - #region PR 2073 - - [Fact] - [Trait("PR", "2073")] // https://github.com/ThreeMammals/Ocelot/pull/2073 - [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 - [Trait("Feat", "1869")] // https://github.com/ThreeMammals/Ocelot/issues/1869 - public void CreateTimeout_HasRouteTimeout_ShouldCreateFromRoute() - { - // Arrange - var route = new FileDynamicRoute { Timeout = 11 }; - var global = new FileGlobalConfiguration { Timeout = 22 }; - - // Act - var timeout = _creator.CreateTimeout(route, global); - - // Assert - Assert.Equal(route.Timeout, timeout); - } - - [Fact] - [Trait("PR", "2073")] - [Trait("Feat", "1314")] - public void CreateTimeout_NoRouteTimeoutAndHasGlobalOne_ShouldCreateFromGlobalConfig() - { - // Arrange - var route = new FileDynamicRoute(); - var global = new FileGlobalConfiguration { Timeout = 22 }; - - // Act - var timeout = _creator.CreateTimeout(route, global); - - // Assert - Assert.Null(route.Timeout); - Assert.Equal(global.Timeout, timeout); - } - - [Fact] - [Trait("PR", "2073")] - [Trait("Feat", "1314")] - public void CreateTimeout_NoRouteTimeoutAndNoGlobalOne_ShouldCreateFromDownstreamRouteDefaults() - { - // Arrange - var route = new FileDynamicRoute(); - var global = new FileGlobalConfiguration(); - - // Act - var timeout = _creator.CreateTimeout(route, global); - - // Assert - Assert.Null(route.Timeout); - Assert.Null(global.Timeout); - Assert.Equal(DownstreamRoute.DefTimeout, timeout); - } - #endregion - - private static FileDynamicRoute GivenDynamicRoute(string serviceName, bool enableRateLimiting, string downstreamHttpVersion, string key, string value) => new() - { - ServiceName = serviceName, - RateLimitRule = new FileRateLimitByHeaderRule - { - EnableRateLimiting = enableRateLimiting, - }, - DownstreamHttpVersion = downstreamHttpVersion, - Metadata = new Dictionary - { - [key] = value, - }, - }; - - private void ThenTheRloCreatorIsCalledCorrectly() - { - _rloCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0], _fileConfig.GlobalConfiguration), - Times.Once); - _rloCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1], _fileConfig.GlobalConfiguration), - Times.Once); - } - - private void ThenTheVersionCreatorIsCalledCorrectly() - { - _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersion), Times.Once); - _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersion), Times.Once); - - _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersionPolicy), Times.Exactly(2)); - _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersionPolicy), Times.Exactly(2)); + } + + #region PR 2073 + + [Fact] + [Trait("PR", "2073")] // https://github.com/ThreeMammals/Ocelot/pull/2073 + [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 + [Trait("Feat", "1869")] // https://github.com/ThreeMammals/Ocelot/issues/1869 + public void CreateTimeout_HasRouteTimeout_ShouldCreateFromRoute() + { + // Arrange + var route = new FileDynamicRoute { Timeout = 11 }; + var global = new FileGlobalConfiguration { Timeout = 22 }; + + // Act + var timeout = _creator.CreateTimeout(route, global); + + // Assert + Assert.Equal(route.Timeout, timeout); + } + + [Fact] + [Trait("PR", "2073")] + [Trait("Feat", "1314")] + public void CreateTimeout_NoRouteTimeoutAndHasGlobalOne_ShouldCreateFromGlobalConfig() + { + // Arrange + var route = new FileDynamicRoute(); + var global = new FileGlobalConfiguration { Timeout = 22 }; + + // Act + var timeout = _creator.CreateTimeout(route, global); + + // Assert + Assert.Null(route.Timeout); + Assert.Equal(global.Timeout, timeout); + } + + [Fact] + [Trait("PR", "2073")] + [Trait("Feat", "1314")] + public void CreateTimeout_NoRouteTimeoutAndNoGlobalOne_ShouldCreateFromDownstreamRouteDefaults() + { + // Arrange + var route = new FileDynamicRoute(); + var global = new FileGlobalConfiguration(); + + // Act + var timeout = _creator.CreateTimeout(route, global); + + // Assert + Assert.Null(route.Timeout); + Assert.Null(global.Timeout); + Assert.Equal(DownstreamRoute.DefTimeout, timeout); + } + #endregion + + private static FileDynamicRoute GivenDynamicRoute(string serviceName, bool enableRateLimiting, string downstreamHttpVersion, string key, string value) => new() + { + ServiceName = serviceName, + RateLimitRule = new FileRateLimitByHeaderRule + { + EnableRateLimiting = enableRateLimiting, + }, + DownstreamHttpVersion = downstreamHttpVersion, + Metadata = new Dictionary + { + [key] = value, + }, + }; + + private void ThenTheBasicCreatorsAreCalledCorrectly() + { + _fileConfig.DynamicRoutes.ForEach(dynamicRoute => + { + _lbKeyCreator.Verify(x => x.Create(dynamicRoute, It.IsAny()), Times.Once); + _lboCreator.Verify(x => x.Create(dynamicRoute, _fileConfig.GlobalConfiguration), Times.Once); + _rloCreator.Verify(x => x.Create(dynamicRoute, _fileConfig.GlobalConfiguration), Times.Once); + }); + } + + private void ThenTheVersionCreatorIsCalledCorrectly() + { + _fileConfig.DynamicRoutes.ForEach(dynamicRoute => + { + _versionCreator.Verify(x => x.Create(dynamicRoute.DownstreamHttpVersion), Times.Once); + _versionPolicyCreator.Verify(x => x.Create(dynamicRoute.DownstreamHttpVersionPolicy), Times.Exactly(2)); + }); } private void ThenTheMetadataCreatorIsCalledCorrectly() { - _metadataCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].Metadata, It.IsAny()), Times.Once); - _metadataCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].Metadata, It.IsAny()), Times.Once); - } - - private void ThenTheRoutesAreReturned() - { - _result.Count.ShouldBe(2); - _result[0].DownstreamRoute[0].RateLimitOptions.EnableRateLimiting.ShouldBeFalse(); - _result[0].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo1); - _result[0].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_version); - _result[0].DownstreamRoute[0].DownstreamHttpVersionPolicy.ShouldBe(_versionPolicy); - _result[0].DownstreamRoute[0].ServiceName.ShouldBe(_fileConfig.DynamicRoutes[0].ServiceName); - - _result[1].DownstreamRoute[0].RateLimitOptions.EnableRateLimiting.ShouldBeTrue(); - _result[1].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo2); - _result[1].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_version); - _result[1].DownstreamRoute[0].DownstreamHttpVersionPolicy.ShouldBe(_versionPolicy); - _result[1].DownstreamRoute[0].ServiceName.ShouldBe(_fileConfig.DynamicRoutes[1].ServiceName); - } - - private void GivenTheVersionCreatorReturns() - { - _version = new Version("1.1"); - _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_version); + _fileConfig.DynamicRoutes.ForEach(dynamicRoute + => _metadataCreator.Verify(x => x.Create(dynamicRoute.Metadata, _fileConfig.GlobalConfiguration), Times.Once)); + } + + private void ThenTheRoutesAreReturned() + { + _result.Count.ShouldBe(2); + for (int i = 0; i < _result.Count; i++) + { + DownstreamRoute dr = _result[i].DownstreamRoute[0]; + dr.RateLimitOptions.EnableRateLimiting.ShouldBe(_rlo[i].EnableRateLimiting); + dr.RateLimitOptions.ShouldBe(_rlo[i]); + dr.DownstreamHttpVersion.ShouldBe(_version); + dr.DownstreamHttpVersionPolicy.ShouldBe(_versionPolicy); + dr.ServiceName.ShouldBe(_fileConfig.DynamicRoutes[i].ServiceName); + } + } + + private void GivenTheVersionCreatorReturns() + { + _version = new Version("1.1"); + _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_version); } private void GivenTheVersionPolicyCreatorReturns() @@ -194,7 +199,7 @@ private void GivenTheVersionPolicyCreatorReturns() _versionPolicy = HttpVersionPolicy.RequestVersionOrLower; _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_versionPolicy); } - + private void GivenTheMetadataCreatorReturns() { _expectedMetadata = new() @@ -202,17 +207,18 @@ private void GivenTheMetadataCreatorReturns() ["foo"] = "bar", }; _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) - .Returns(new MetadataOptions(new FileMetadataOptions{Metadata = _expectedMetadata})); - } - - private void GivenTheRloCreatorReturns() - { - _rlo1 = new() { EnableRateLimiting = false }; - _rlo2 = new() { EnableRateLimiting = true }; - - _rloCreator - .SetupSequence(x => x.Create(It.IsAny(), It.IsAny())) - .Returns(_rlo1) - .Returns(_rlo2); - } -} + .Returns(new MetadataOptions() { Metadata = _expectedMetadata }); + } + + private void GivenTheRloCreatorReturns() + { + _rlo = [ + new() { EnableRateLimiting = false }, + new() { EnableRateLimiting = true }, + ]; + _rloCreator + .SetupSequence(x => x.Create(It.IsAny(), It.IsAny())) + .Returns(_rlo[0]) + .Returns(_rlo[1]); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs index 2cc96f648..9b0df83d1 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs @@ -38,12 +38,15 @@ public async Task Should_set_configuration() string.Empty, serviceProviderConfig, "asdf", - new LoadBalancerOptionsBuilder().Build(), + new LoadBalancerOptions(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); + HttpVersionPolicy.RequestVersionOrLower, + new MetadataOptions(), + new RateLimitOptions(), + 111); GivenTheRepoReturns(new OkResponse()); GivenTheCreatorReturns(new OkResponse(config)); diff --git a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index f132881f5..d6089369d 100644 --- a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -80,10 +80,10 @@ private void ThenTheDependenciesAreCalledCorrectly() private void GivenTheDependenciesAreSetUp() { - _routes = new List { new RouteBuilder().Build() }; - _aggregates = new List { new RouteBuilder().Build() }; - _dynamics = new List { new RouteBuilder().Build() }; - _internalConfig = new InternalConfiguration(null, string.Empty, null, string.Empty, null, string.Empty, null, null, null, null); + _routes = new List { new() }; + _aggregates = new List { new() }; + _dynamics = new List { new() }; + _internalConfig = new InternalConfiguration(); _routesCreator.Setup(x => x.Create(It.IsAny())).Returns(_routes); _aggregatesCreator.Setup(x => x.Create(It.IsAny(), It.IsAny>())).Returns(_aggregates); diff --git a/test/Ocelot.UnitTests/Configuration/FileModels/FileDynamicRouteTests.cs b/test/Ocelot.UnitTests/Configuration/FileModels/FileDynamicRouteTests.cs index 9b0346c12..bc5418cdd 100644 --- a/test/Ocelot.UnitTests/Configuration/FileModels/FileDynamicRouteTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileModels/FileDynamicRouteTests.cs @@ -11,8 +11,10 @@ public void Ctor() FileDynamicRoute instance = new(); // Assert - Assert.NotNull(instance.Metadata); - Assert.Empty(instance.Metadata); + Assert.Null(instance.Metadata); + Assert.Null(instance.Key); + Assert.Null(instance.RateLimitRule); + Assert.Null(instance.RateLimitOptions); } [Fact] @@ -27,28 +29,6 @@ public void Ctor_IRouteGrouping_IsImplemented() Assert.Equal("abc", obj.Key); } - [Fact] - public void Ctor_IRouteUpstream_IsImplemented() - { - // Arrange, Act - FileDynamicRoute instance = new() - { - ServiceName = "1", - UpstreamHttpMethod = ["2"], - }; - - // Assert - Assert.IsAssignableFrom(instance); - IRouteUpstream obj = instance; - Assert.NotNull(obj.UpstreamHeaderTemplates); - Assert.Empty(obj.UpstreamHeaderTemplates); - Assert.Equal(instance.ServiceName, obj.UpstreamPathTemplate); - Assert.NotNull(obj.UpstreamHttpMethod); - Assert.Contains("2", obj.UpstreamHttpMethod); - Assert.False(obj.RouteIsCaseSensitive); - Assert.Equal(0, obj.Priority); - } - [Fact] public void Ctor_IRouteRateLimiting_IsImplemented() { diff --git a/test/Ocelot.UnitTests/Configuration/FileModels/FileMetadataOptionsTests.cs b/test/Ocelot.UnitTests/Configuration/FileModels/FileMetadataOptionsTests.cs new file mode 100644 index 000000000..03b4f7fbe --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/FileModels/FileMetadataOptionsTests.cs @@ -0,0 +1,44 @@ +using Ocelot.Configuration.File; +using System.Globalization; + +namespace Ocelot.UnitTests.Configuration.FileModels; + +public class FileMetadataOptionsTests +{ + [Fact] + public void Ctor_Default() + { + // Arrange, Act + FileMetadataOptions actual = new(); + + // Assert + Assert.Contains(",", actual.Separators); + Assert.Contains(" ", actual.TrimChars); + } + + [Fact] + public void Ctor_CopyingFrom() + { + // Arrange + FileMetadataOptions from = new() + { + CurrentCulture = CultureInfo.GetCultureInfo("uk").Name, + NumberStyle = NumberStyles.None.ToString(), + Separators = ["|"], + StringSplitOption = StringSplitOptions.TrimEntries.ToString(), + TrimChars = ['x'], + }; + + // Act + FileMetadataOptions actual = new(from); + + // Assert + Assert.False(ReferenceEquals(from, actual)); + Assert.Equivalent(from, actual); + Assert.Equal("uk", actual.CurrentCulture); + Assert.Equal("None", actual.NumberStyle); + Assert.Contains("|", actual.Separators); + Assert.Equal("TrimEntries", actual.StringSplitOption); + Assert.Contains('x', actual.TrimChars); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/FileModels/FileRouteTests.cs b/test/Ocelot.UnitTests/Configuration/FileModels/FileRouteTests.cs index c1dfc91ba..4abed3e6e 100644 --- a/test/Ocelot.UnitTests/Configuration/FileModels/FileRouteTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileModels/FileRouteTests.cs @@ -129,8 +129,8 @@ private static FileRoute GivenFileRoute() expected.FileCacheOptions.Header = "value14"; expected.HttpHandlerOptions.MaxConnectionsPerServer = 15; expected.Key = "value16"; - expected.LoadBalancerOptions.Key = "value17"; - expected.Metadata.Add("key18", "value18"); + expected.LoadBalancerOptions ??= new("value17"); + expected.Metadata ??= new Dictionary() { { "key18", "value18" } }; expected.Priority = 19; expected.QoSOptions.DurationOfBreak = 20; expected.RateLimitOptions ??= new() { Period = "value21" }; diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs index ed85feb71..97d829825 100644 --- a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -70,10 +70,7 @@ public List Routes return new List { - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List {"Get"}) - .Build(), + new(downstreamRoute, HttpMethod.Get), }; } } @@ -86,6 +83,9 @@ public List Routes public QoSOptions QoSOptions { get; } public HttpHandlerOptions HttpHandlerOptions { get; } public Version DownstreamHttpVersion { get; } - public HttpVersionPolicy? DownstreamHttpVersionPolicy { get; } + public HttpVersionPolicy DownstreamHttpVersionPolicy { get; } + public MetadataOptions MetadataOptions => throw new NotImplementedException(); + public RateLimitOptions RateLimitOptions => throw new NotImplementedException(); + public int? Timeout => throw new NotImplementedException(); } } diff --git a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs deleted file mode 100644 index fe831c278..000000000 --- a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; - -namespace Ocelot.UnitTests.Configuration; - -public class LoadBalancerOptionsCreatorTests : UnitTest -{ - private readonly LoadBalancerOptionsCreator _creator = new(); - - [Fact] - public void Should_create() - { - // Arrange - var options = new FileLoadBalancerOptions - { - Type = "test", - Key = "west", - Expiry = 1, - }; - - // Act - var result = _creator.Create(options); - - // Assert - result.Type.ShouldBe(options.Type); - result.Key.ShouldBe(options.Key); - result.ExpiryInMs.ShouldBe(options.Expiry); - } -} diff --git a/test/Ocelot.UnitTests/Configuration/MetadataOptionsTests.cs b/test/Ocelot.UnitTests/Configuration/MetadataOptionsTests.cs new file mode 100644 index 000000000..5a6480440 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/MetadataOptionsTests.cs @@ -0,0 +1,74 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.File; +using System.Globalization; + +namespace Ocelot.UnitTests.Configuration; + +public class MetadataOptionsTests +{ + [Fact] + public void Ctor_Parameterless() + { + // Arrange, Act + MetadataOptions actual = new(); + + // Assert + Assert.Equal(CultureInfo.CurrentCulture, actual.CurrentCulture); + Assert.Equal(NumberStyles.Any, actual.NumberStyle); + Assert.Contains(",", actual.Separators); + Assert.Equal(StringSplitOptions.None, actual.StringSplitOption); + Assert.Contains(' ', actual.TrimChars); + Assert.NotNull(actual.Metadata); + } + + [Fact] + [Trait("PR", "2324")] + public void Ctor_CopyingFrom_MetadataOptions() + { + // Arrange + MetadataOptions from = new( + separators: ["x"], + trimChars: ['y'], + stringSplitOption: StringSplitOptions.TrimEntries, + numberStyle: NumberStyles.Number, + currentCulture: CultureInfo.GetCultureInfo("uk"), + metadata: new Dictionary() + { + { "key", "value" }, + }); + + // Act + MetadataOptions actual = new(from); + + // Assert + Assert.False(ReferenceEquals(from, actual)); + Assert.Equivalent(from, actual); + } + + [Fact] + [Trait("PR", "2324")] + public void Ctor_CopyingFrom_FileMetadataOptions() + { + // Arrange + FileMetadataOptions from = new() + { + CurrentCulture = "uk", + NumberStyle = nameof(NumberStyles.Number), + Separators = ["x"], + StringSplitOption = nameof(StringSplitOptions.RemoveEmptyEntries), + TrimChars = [';'], + }; + + // Act + MetadataOptions actual = new(from); + + // Assert + Assert.False(ReferenceEquals(from, actual)); + Assert.Equal(CultureInfo.GetCultureInfo("uk"), actual.CurrentCulture); + Assert.Equal(NumberStyles.Number, actual.NumberStyle); + Assert.Contains("x", actual.Separators); + Assert.Equal(StringSplitOptions.RemoveEmptyEntries, actual.StringSplitOption); + Assert.Contains(';', actual.TrimChars); + Assert.NotNull(actual.Metadata); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs index d44771cbd..1203e54fc 100644 --- a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs @@ -106,6 +106,49 @@ public void HasRouteOptions_ShouldCreateFromRouteQosOptions() AssertEquality(actual, expected); } + [Fact] + public void Create_FileDynamicRoute_FileGlobalConfiguration() + { + // Arrange + FileGlobalConfiguration global = new() + { + QoSOptions = new() + { + DurationOfBreak = 1, + ExceptionsAllowedBeforeBreaking = 2, + FailureRatio = 3.0D, + SamplingDuration = 4, + TimeoutValue = 5, + }, + }; + FileDynamicRoute route = new() + { + QoSOptions = new FileQoSOptions + { + DurationOfBreak = 10, + ExceptionsAllowedBeforeBreaking = 20, + FailureRatio = 30.0D, + SamplingDuration = 40, + TimeoutValue = 50, + }, + }; + QoSOptions expected = new(route.QoSOptions); + + // Act + var actual = _creator.Create(route, global); + + // Assert + Assert.Equivalent(expected, actual); + AssertEquality(actual, expected); + + // Should create from global + route.QoSOptions = null; + expected = new(global.QoSOptions); + actual = _creator.Create(route, global); + Assert.Equivalent(expected, actual); + AssertEquality(actual, expected); + } + private static void AssertEquality(QoSOptions actual, QoSOptions expected) { Assert.Equal(expected.DurationOfBreak, actual.DurationOfBreak); diff --git a/test/Ocelot.UnitTests/Configuration/QoSOptionsTests.cs b/test/Ocelot.UnitTests/Configuration/QoSOptionsTests.cs index 3a4eca0cd..3f742378c 100644 --- a/test/Ocelot.UnitTests/Configuration/QoSOptionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/QoSOptionsTests.cs @@ -14,7 +14,6 @@ public void Ctor_Copy_ShouldCopy() .WithExceptionsAllowedBeforeBreaking(1) .WithDurationOfBreak(2) .WithTimeoutValue(3) - .WithKey("123") .WithFailureRatio(4.0D) .WithSamplingDuration(5) .Build(); @@ -27,7 +26,6 @@ public void Ctor_Copy_ShouldCopy() Assert.Equal(copyee.ExceptionsAllowedBeforeBreaking, actual.ExceptionsAllowedBeforeBreaking); Assert.Equal(copyee.DurationOfBreak, actual.DurationOfBreak); Assert.Equal(copyee.TimeoutValue, actual.TimeoutValue); - Assert.Equal(copyee.Key, actual.Key); Assert.Equal(copyee.FailureRatio, actual.FailureRatio); Assert.Equal(copyee.SamplingDuration, actual.SamplingDuration); } diff --git a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs index 57c6b32a6..d46c44a1b 100644 --- a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs @@ -1,6 +1,7 @@ +using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; namespace Ocelot.UnitTests.Configuration; @@ -12,17 +13,11 @@ public class RouteKeyCreatorTests : UnitTest public void Should_return_sticky_session_key() { // Arrange - var route = new FileRoute - { - LoadBalancerOptions = new FileLoadBalancerOptions - { - Key = "testy", - Type = nameof(CookieStickySessions), - }, - }; + FileRoute route = new(); + LoadBalancerOptions options = new(nameof(CookieStickySessions), "testy", null); // Act - var result = _creator.Create(route); + var result = _creator.Create(route, options); // Assert result.ShouldBe("CookieStickySessions:testy"); @@ -42,12 +37,13 @@ public void Should_return_route_key() new("localhost", 4430), }, }; + LoadBalancerOptions options = new(); // Act - var result = _creator.Create(route); + var result = _creator.Create(route, options); // Assert - result.ShouldBe("GET,POST,PUT|/api/product|no-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|no-lb-type|no-lb-key"); + result.ShouldBe("GET,POST,PUT|/api/product|no-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|NoLoadBalancer|no-lb-key"); } [Fact] @@ -65,12 +61,13 @@ public void Should_return_route_key_with_upstream_host() new("localhost", 4430), }, }; + LoadBalancerOptions options = new(); // Act - var result = _creator.Create(route); + var result = _creator.Create(route, options); // Assert - result.ShouldBe("GET,POST,PUT|/api/product|my-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|no-lb-type|no-lb-key"); + result.ShouldBe("GET,POST,PUT|/api/product|my-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|NoLoadBalancer|no-lb-key"); } [Fact] @@ -83,12 +80,13 @@ public void Should_return_route_key_with_svc_name() UpstreamHttpMethod = ["GET", "POST", "PUT"], ServiceName = "products-service", }; + LoadBalancerOptions options = new(); // Act - var result = _creator.Create(route); + var result = _creator.Create(route, options); // Assert - result.ShouldBe("GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|no-lb-type|no-lb-key"); + result.ShouldBe("GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|NoLoadBalancer|no-lb-key"); } [Fact] @@ -106,11 +104,125 @@ public void Should_return_route_key_with_load_balancer_options() Key = "testy", }, }; + LoadBalancerOptions options = new(route.LoadBalancerOptions); // Act - var result = _creator.Create(route); + var result = _creator.Create(route, options); // Assert result.ShouldBe("GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|LeastConnection|testy"); } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + public void Create_FileDynamicRoute_TryStickySession() + { + // Arrange + FileDynamicRoute route = new(); + LoadBalancerOptions options = new(nameof(CookieStickySessions), "TestKey", null); + + // Act + var actual = _creator.Create(route, options); + + // Assert + Assert.Equal("CookieStickySessions:TestKey", actual); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + public void Create_FileDynamicRoute_HasLoadBalancingKey() + { + // Arrange + FileDynamicRoute route = new(); + LoadBalancerOptions options = new(nameof(RoundRobin), "LBKey", null); + + // Act + var actual = _creator.Create(route, options); + + // Assert + Assert.Equal("LBKey", actual); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + public void Create_FileDynamicRoute_NoLBKey() + { + // Arrange + FileDynamicRoute route = new() + { + ServiceName = "test", + ServiceNamespace = "namespace", + }; + LoadBalancerOptions options = new(nameof(RoundRobin), null, null); + + // Act + var actual = _creator.Create(route, options); + + // Assert + Assert.Equal("namespace.test", actual); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + public void Create_String_String_TryStickySession() + { + // Arrange + LoadBalancerOptions options = new(nameof(CookieStickySessions), "TestKey", null); + + // Act + var actual = _creator.Create("namespace", "service", options); + + // Assert + Assert.Equal("CookieStickySessions:TestKey", actual); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + public void Create_String_String_HasLoadBalancingKey() + { + // Arrange + LoadBalancerOptions options = new(nameof(RoundRobin), "LBKey", null); + + // Act + var actual = _creator.Create("namespace", "service", options); + + // Assert + Assert.Equal("LBKey", actual); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + public void Create_String_String_NoLBKey() + { + // Arrange + LoadBalancerOptions options = new(nameof(RoundRobin), null, null); + + // Act + var actual = _creator.Create("namespace", "service", options); + + // Assert + Assert.Equal("namespace.service", actual); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] + public void AsString() + { + // Arrange, Act, Assert + FileHostAndPort host = null; + var actual = RouteKeyCreator.AsString(host); + Assert.Null(actual); + + // Arrange, Act, Assert + host = new("test.host", 123); + actual = RouteKeyCreator.AsString(host); + Assert.Equal("test.host:123", actual); + } } diff --git a/test/Ocelot.UnitTests/Configuration/RouteTests.cs b/test/Ocelot.UnitTests/Configuration/RouteTests.cs new file mode 100644 index 000000000..5a5694cf3 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/RouteTests.cs @@ -0,0 +1,84 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; + +namespace Ocelot.UnitTests.Configuration; + +public class RouteTests +{ + [Fact] + public void Ctor() + { + // Arrange, Act + Route r = new(); + + // Assert + Assert.NotNull(r.DownstreamRoute); + Assert.Empty(r.DownstreamRoute); + + Assert.False(r.IsDynamic); + Assert.Null(r.Aggregator); + Assert.NotNull(r.DownstreamRoute); + Assert.Null(r.DownstreamRouteConfig); + Assert.Null(r.UpstreamHeaderTemplates); + Assert.Null(r.UpstreamHost); + Assert.Null(r.UpstreamHttpMethod); + Assert.Null(r.UpstreamTemplatePattern); + } + + [Fact] + public void Ctor_Boolean() + { + // Arrange, Act + Route r1 = new(true), + r2 = new(false); + + Assert.True(r1.IsDynamic); + Assert.False(r2.IsDynamic); + Assert.Empty(r1.DownstreamRoute); + Assert.Empty(r2.DownstreamRoute); + } + + [Fact] + public void Ctor_Boolean_DownstreamRoute() + { + // Arrange + DownstreamRoute route = new DownstreamRouteBuilder().Build(); + + // Act + Route r = new(true, route); + + Assert.True(r.IsDynamic); + Assert.NotEmpty(r.DownstreamRoute); + Assert.Equal(route, r.DownstreamRoute[0]); + } + + [Fact] + public void Ctor_DownstreamRoute() + { + // Arrange + DownstreamRoute route = new DownstreamRouteBuilder().Build(); + + // Act + Route r = new(route); + + Assert.False(r.IsDynamic); + Assert.NotEmpty(r.DownstreamRoute); + Assert.Equal(route, r.DownstreamRoute[0]); + } + + [Fact] + public void Ctor_DownstreamRoute_HttpMethod() + { + // Arrange + DownstreamRoute route = new DownstreamRouteBuilder().Build(); + HttpMethod method = HttpMethod.Connect; + + // Act + Route r = new(route, method); + + Assert.NotEmpty(r.DownstreamRoute); + Assert.Equal(route, r.DownstreamRoute[0]); + Assert.NotEmpty(r.UpstreamHttpMethod); + Assert.Equal(method, r.UpstreamHttpMethod.First()); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/StaticRoutesCreatorTests.cs similarity index 90% rename from test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs rename to test/Ocelot.UnitTests/Configuration/StaticRoutesCreatorTests.cs index 3a762b114..1527587c8 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/StaticRoutesCreatorTests.cs @@ -3,371 +3,365 @@ using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Values; - -namespace Ocelot.UnitTests.Configuration; - -public class RoutesCreatorTests : UnitTest -{ - private readonly RoutesCreator _creator; - private readonly Mock _cthCreator; - private readonly Mock _aoCreator; - private readonly Mock _utpCreator; - private readonly Mock _uhtpCreator; - private readonly Mock _ridkCreator; - private readonly Mock _qosoCreator; - private readonly Mock _rroCreator; - private readonly Mock _rloCreator; - private readonly Mock _coCreator; - private readonly Mock _hhoCreator; - private readonly Mock _hfarCreator; - private readonly Mock _daCreator; - private readonly Mock _lboCreator; - private readonly Mock _rrkCreator; - private readonly Mock _soCreator; - private readonly Mock _versionCreator; + +namespace Ocelot.UnitTests.Configuration; + +public class StaticRoutesCreatorTests : UnitTest +{ + private readonly StaticRoutesCreator _creator; + private readonly Mock _cthCreator; + private readonly Mock _aoCreator; + private readonly Mock _utpCreator; + private readonly Mock _uhtpCreator; + private readonly Mock _ridkCreator; + private readonly Mock _qosoCreator; + private readonly Mock _rroCreator; + private readonly Mock _rloCreator; + private readonly Mock _coCreator; + private readonly Mock _hhoCreator; + private readonly Mock _hfarCreator; + private readonly Mock _daCreator; + private readonly Mock _lboCreator; + private readonly Mock _rrkCreator; + private readonly Mock _soCreator; + private readonly Mock _versionCreator; private readonly Mock _versionPolicyCreator; private readonly Mock _metadataCreator; - private FileConfiguration _fileConfig; - private RouteOptions _rro; - private string _requestId; - private string _rrk; - private UpstreamPathTemplate _upt; - private AuthenticationOptions _ao; - private List _ctt; - private QoSOptions _qoso; - private RateLimitOptions _rlo; - private CacheOptions _cacheOptions; - private HttpHandlerOptions _hho; - private HeaderTransformations _ht; - private List _dhp; - private LoadBalancerOptions _lbo; - private IReadOnlyList _result; + private FileConfiguration _fileConfig; + private RouteOptions _rro; + private string _requestId; + private string _rrk; + private UpstreamPathTemplate _upt; + private AuthenticationOptions _ao; + private List _ctt; + private QoSOptions _qoso; + private RateLimitOptions _rlo; + private CacheOptions _cacheOptions; + private HttpHandlerOptions _hho; + private HeaderTransformations _ht; + private List _dhp; + private LoadBalancerOptions _lbo; + private IReadOnlyList _result; private Version _expectedVersion; private HttpVersionPolicy _expectedVersionPolicy; - private Dictionary _uht; - private Dictionary _expectedMetadata; - - public RoutesCreatorTests() - { - _cthCreator = new Mock(); - _aoCreator = new Mock(); - _utpCreator = new Mock(); - _ridkCreator = new Mock(); - _qosoCreator = new Mock(); - _rroCreator = new Mock(); - _rloCreator = new Mock(); - _coCreator = new Mock(); - _hhoCreator = new Mock(); - _hfarCreator = new Mock(); - _daCreator = new Mock(); - _lboCreator = new Mock(); - _rrkCreator = new Mock(); - _soCreator = new Mock(); - _versionCreator = new Mock(); + private Dictionary _uht; + private Dictionary _expectedMetadata; + + public StaticRoutesCreatorTests() + { + _cthCreator = new Mock(); + _aoCreator = new Mock(); + _utpCreator = new Mock(); + _ridkCreator = new Mock(); + _qosoCreator = new Mock(); + _rroCreator = new Mock(); + _rloCreator = new Mock(); + _coCreator = new Mock(); + _hhoCreator = new Mock(); + _hfarCreator = new Mock(); + _daCreator = new Mock(); + _lboCreator = new Mock(); + _rrkCreator = new Mock(); + _soCreator = new Mock(); + _versionCreator = new Mock(); _versionPolicyCreator = new Mock(); - _uhtpCreator = new Mock(); + _uhtpCreator = new Mock(); _metadataCreator = new Mock(); - - _creator = new RoutesCreator( - _cthCreator.Object, - _aoCreator.Object, - _utpCreator.Object, - _ridkCreator.Object, - _qosoCreator.Object, - _rroCreator.Object, - _rloCreator.Object, - _coCreator.Object, - _hhoCreator.Object, - _hfarCreator.Object, - _daCreator.Object, - _lboCreator.Object, - _rrkCreator.Object, - _soCreator.Object, + + _creator = new StaticRoutesCreator( + _cthCreator.Object, + _aoCreator.Object, + _utpCreator.Object, + _ridkCreator.Object, + _qosoCreator.Object, + _rroCreator.Object, + _rloCreator.Object, + _coCreator.Object, + _hhoCreator.Object, + _hfarCreator.Object, + _daCreator.Object, + _lboCreator.Object, + _rrkCreator.Object, + _soCreator.Object, _versionCreator.Object, - _versionPolicyCreator.Object, - _uhtpCreator.Object, - _metadataCreator.Object); - } - - [Fact] - public void Should_return_nothing() - { - // Arrange - _fileConfig = new FileConfiguration(); - - // Act - _result = _creator.Create(_fileConfig); - - // Assert - _result.ShouldBeEmpty(); - } - - [Fact] - public void Should_return_routes() - { - // Arrange - _fileConfig = new FileConfiguration - { - Routes = new List - { - new() - { - ServiceName = "dave", - DangerousAcceptAnyServerCertificateValidator = true, - AddClaimsToRequest = new Dictionary - { - { "a","b" }, - }, - AddHeadersToRequest = new Dictionary - { - { "c","d" }, - }, - AddQueriesToRequest = new Dictionary - { - { "e","f" }, - }, - UpstreamHttpMethod = new List { "GET", "POST" }, - Metadata = new Dictionary - { - ["foo"] = "bar", - }, - }, - new() - { - ServiceName = "wave", - DangerousAcceptAnyServerCertificateValidator = false, - AddClaimsToRequest = new Dictionary - { - { "g","h" }, - }, - AddHeadersToRequest = new Dictionary - { - { "i","j" }, - }, - AddQueriesToRequest = new Dictionary - { - { "k","l" }, - }, - UpstreamHttpMethod = new List { "PUT", "DELETE" }, - Metadata = new Dictionary - { - ["foo"] = "baz", + _versionPolicyCreator.Object, + _uhtpCreator.Object, + _metadataCreator.Object); + } + + [Fact] + public void Should_return_nothing() + { + // Arrange + _fileConfig = new FileConfiguration(); + + // Act + _result = _creator.Create(_fileConfig); + + // Assert + _result.ShouldBeEmpty(); + } + + [Fact] + public void Should_return_routes() + { + // Arrange + _fileConfig = new FileConfiguration + { + Routes = new List + { + new() + { + ServiceName = "dave", + DangerousAcceptAnyServerCertificateValidator = true, + AddClaimsToRequest = new Dictionary + { + { "a","b" }, + }, + AddHeadersToRequest = new Dictionary + { + { "c","d" }, + }, + AddQueriesToRequest = new Dictionary + { + { "e","f" }, + }, + UpstreamHttpMethod = ["GET", "POST"], + Metadata = new Dictionary + { + ["foo"] = "bar", + }, + LoadBalancerOptions = new("LB1"), + }, + new() + { + ServiceName = "wave", + DangerousAcceptAnyServerCertificateValidator = false, + AddClaimsToRequest = new Dictionary + { + { "g","h" }, }, - }, - }, - }; - GivenTheDependenciesAreSetUpCorrectly(); - - // Act - _result = _creator.Create(_fileConfig); - - // Assert - ThenTheDependenciesAreCalledCorrectly(); - ThenTheRoutesAreCreated(); - } - - #region PR 2073 - - [Fact] - [Trait("PR", "2073")] // https://github.com/ThreeMammals/Ocelot/pull/2073 - [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 - [Trait("Feat", "1869")] // https://github.com/ThreeMammals/Ocelot/issues/1869 - public void CreateTimeout_HasRouteTimeout_ShouldCreateFromRoute() - { - // Arrange - var route = new FileRoute { Timeout = 11 }; - var global = new FileGlobalConfiguration { Timeout = 22 }; - - // Act - var timeout = _creator.CreateTimeout(route, global); - - // Assert - Assert.Equal(route.Timeout, timeout); - } - - [Fact] - [Trait("PR", "2073")] - [Trait("Feat", "1314")] - public void CreateTimeout_NoRouteTimeoutAndHasGlobalOne_ShouldCreateFromGlobalConfig() - { - // Arrange - var route = new FileRoute(); - var global = new FileGlobalConfiguration { Timeout = 22 }; - - // Act - var timeout = _creator.CreateTimeout(route, global); - - // Assert - Assert.Null(route.Timeout); - Assert.Equal(global.Timeout, timeout); - } - - [Fact] - [Trait("PR", "2073")] - [Trait("Feat", "1314")] - public void CreateTimeout_NoRouteTimeoutAndNoGlobalOne_ShouldCreateFromDownstreamRouteDefaults() - { - // Arrange - var route = new FileRoute(); - var global = new FileGlobalConfiguration(); - - // Act - var timeout = _creator.CreateTimeout(route, global); - - // Assert - Assert.Null(route.Timeout); - Assert.Null(global.Timeout); - Assert.Equal(DownstreamRoute.DefTimeout, timeout); - } - #endregion - - [Theory] - [Trait("PR", "2294")] - [InlineData("", 0)] - [InlineData("A", 1)] - [InlineData("A,B", 2)] - [InlineData(" X ", 1)] - public void SetUpRoute_FileRouteUpstreamHttpMethod(string methods, int count) - { - // Arrange - _fileConfig = new FileConfiguration(); - _fileConfig.Routes.Add(new() { UpstreamHttpMethod = methods.Split(',').Where(m => !string.IsNullOrEmpty(m)).ToList() }); - GivenTheDependenciesAreSetUpCorrectly(); - - // Act - _result = _creator.Create(_fileConfig); - - // Assert - Assert.Equal(count, _result[0].UpstreamHttpMethod.Count); - } - - private void ThenTheDependenciesAreCalledCorrectly() - { - ThenTheDepsAreCalledFor(_fileConfig.Routes[0], _fileConfig.GlobalConfiguration); - ThenTheDepsAreCalledFor(_fileConfig.Routes[1], _fileConfig.GlobalConfiguration); - } - - private void GivenTheDependenciesAreSetUpCorrectly() - { - _expectedVersion = new Version("1.1"); + AddHeadersToRequest = new Dictionary + { + { "i","j" }, + }, + AddQueriesToRequest = new Dictionary + { + { "k","l" }, + }, + UpstreamHttpMethod = ["PUT", "DELETE"], + Metadata = new Dictionary + { + ["foo"] = "baz", + }, + LoadBalancerOptions = new("LB2"), + }, + }, + }; + GivenTheDependenciesAreSetUpCorrectly(); + + // Act + _result = _creator.Create(_fileConfig); + + // Assert + ThenTheDependenciesAreCalledCorrectly(); + ThenTheRoutesAreCreated(); + } + + #region PR 2073 + + [Fact] + [Trait("PR", "2073")] // https://github.com/ThreeMammals/Ocelot/pull/2073 + [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 + [Trait("Feat", "1869")] // https://github.com/ThreeMammals/Ocelot/issues/1869 + public void CreateTimeout_HasRouteTimeout_ShouldCreateFromRoute() + { + // Arrange + var route = new FileRoute { Timeout = 11 }; + var global = new FileGlobalConfiguration { Timeout = 22 }; + + // Act + var timeout = _creator.CreateTimeout(route, global); + + // Assert + Assert.Equal(route.Timeout, timeout); + } + + [Fact] + [Trait("PR", "2073")] + [Trait("Feat", "1314")] + public void CreateTimeout_NoRouteTimeoutAndHasGlobalOne_ShouldCreateFromGlobalConfig() + { + // Arrange + var route = new FileRoute(); + var global = new FileGlobalConfiguration { Timeout = 22 }; + + // Act + var timeout = _creator.CreateTimeout(route, global); + + // Assert + Assert.Null(route.Timeout); + Assert.Equal(global.Timeout, timeout); + } + + [Fact] + [Trait("PR", "2073")] + [Trait("Feat", "1314")] + public void CreateTimeout_NoRouteTimeoutAndNoGlobalOne_ShouldCreateFromDownstreamRouteDefaults() + { + // Arrange + var route = new FileRoute(); + var global = new FileGlobalConfiguration(); + + // Act + var timeout = _creator.CreateTimeout(route, global); + + // Assert + Assert.Null(route.Timeout); + Assert.Null(global.Timeout); + Assert.Equal(DownstreamRoute.DefTimeout, timeout); + } + #endregion + + [Theory] + [Trait("PR", "2294")] + [InlineData("", 0)] + [InlineData("A", 1)] + [InlineData("A,B", 2)] + [InlineData(" X ", 1)] + public void SetUpRoute_FileRouteUpstreamHttpMethod(string methods, int count) + { + // Arrange + _fileConfig = new FileConfiguration(); + _fileConfig.Routes.Add(new() { UpstreamHttpMethod = methods.Split(',').Where(m => !string.IsNullOrEmpty(m)).ToHashSet() }); + GivenTheDependenciesAreSetUpCorrectly(); + + // Act + _result = _creator.Create(_fileConfig); + + // Assert + Assert.Equal(count, _result[0].UpstreamHttpMethod.Count); + } + + private void ThenTheDependenciesAreCalledCorrectly() + { + ThenTheDepsAreCalledFor(_fileConfig.Routes[0], _fileConfig.GlobalConfiguration); + ThenTheDepsAreCalledFor(_fileConfig.Routes[1], _fileConfig.GlobalConfiguration); + } + + private void GivenTheDependenciesAreSetUpCorrectly() + { + _expectedVersion = new Version("1.1"); _expectedVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; - _rro = new RouteOptions(false, false, false, false); - _requestId = "testy"; - _rrk = "besty"; - _upt = new UpstreamPathTemplateBuilder().Build(); - _ao = new AuthenticationOptionsBuilder().Build(); - _ctt = new List(); - _qoso = new QoSOptionsBuilder().Build(); - _rlo = new RateLimitOptions(); - - _cacheOptions = new CacheOptions(0, "vesty", null, false); - _hho = new HttpHandlerOptionsBuilder().Build(); - _ht = new HeaderTransformations(new List(), new List(), new List(), new List()); - _dhp = new List(); - _lbo = new LoadBalancerOptionsBuilder().Build(); - _uht = new Dictionary(); + _rro = new RouteOptions(false, false, false, false); + _requestId = "testy"; + _rrk = "besty"; + _upt = new UpstreamPathTemplateBuilder().Build(); + _ao = new AuthenticationOptionsBuilder().Build(); + _ctt = new List(); + _qoso = new QoSOptionsBuilder().Build(); + _rlo = new RateLimitOptions(); + + _cacheOptions = new CacheOptions(0, "vesty", null, false); + _hho = new HttpHandlerOptionsBuilder().Build(); + _ht = new HeaderTransformations(new List(), new List(), new List(), new List()); + _dhp = new List(); + _lbo = new(); + _uht = new Dictionary(); _expectedMetadata = new Dictionary() { ["foo"] = "bar", }; - - _rroCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rro); - _ridkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_requestId); - _rrkCreator.Setup(x => x.Create(It.IsAny())).Returns(_rrk); - _utpCreator.Setup(x => x.Create(It.IsAny())).Returns(_upt); - _aoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_ao); - _cthCreator.Setup(x => x.Create(It.IsAny>())).Returns(_ctt); - _qosoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_qoso); - _rloCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rlo); - _coCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())).Returns(_cacheOptions); - _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); - _hfarCreator.Setup(x => x.Create(It.IsAny())).Returns(_ht); - _hfarCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_ht); - _daCreator.Setup(x => x.Create(It.IsAny())).Returns(_dhp); - _lboCreator.Setup(x => x.Create(It.IsAny())).Returns(_lbo); - _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); + + _rroCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rro); + _ridkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_requestId); + _rrkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rrk); + _utpCreator.Setup(x => x.Create(It.IsAny())).Returns(_upt); + _aoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_ao); + _cthCreator.Setup(x => x.Create(It.IsAny>())).Returns(_ctt); + _qosoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_qoso); + _rloCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rlo); + _coCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())).Returns(_cacheOptions); + _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); + _hfarCreator.Setup(x => x.Create(It.IsAny())).Returns(_ht); + _hfarCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_ht); + _daCreator.Setup(x => x.Create(It.IsAny())).Returns(_dhp); + _lboCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_lbo); + _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersionPolicy); - _uhtpCreator.Setup(x => x.Create(It.IsAny())).Returns(_uht); - _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())).Returns(new MetadataOptions(new FileMetadataOptions - { - Metadata = _expectedMetadata, - })); - } - - private void ThenTheRoutesAreCreated() - { - _result.Count.ShouldBe(2); - ThenTheRouteIsSet(_fileConfig.Routes[0], 0); - ThenTheRouteIsSet(_fileConfig.Routes[1], 1); - } - - private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) - { - _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_expectedVersion); + _uhtpCreator.Setup(x => x.Create(It.IsAny())).Returns(_uht); + _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) + .Returns(new MetadataOptions() { Metadata = _expectedMetadata }); + } + + private void ThenTheRoutesAreCreated() + { + _result.Count.ShouldBe(2); + ThenTheRouteIsSet(_fileConfig.Routes[0], 0); + ThenTheRouteIsSet(_fileConfig.Routes[1], 1); + } + + private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) + { + _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_expectedVersion); _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersionPolicy.ShouldBe(_expectedVersionPolicy); - _result[routeIndex].DownstreamRoute[0].IsAuthenticated.ShouldBe(_rro.IsAuthenticated); - _result[routeIndex].DownstreamRoute[0].IsAuthorized.ShouldBe(_rro.IsAuthorized); - _result[routeIndex].DownstreamRoute[0].IsCached.ShouldBe(_rro.IsCached); - _result[routeIndex].DownstreamRoute[0].RequestIdKey.ShouldBe(_requestId); - _result[routeIndex].DownstreamRoute[0].LoadBalancerKey.ShouldBe(_rrk); - _result[routeIndex].DownstreamRoute[0].UpstreamPathTemplate.ShouldBe(_upt); - _result[routeIndex].DownstreamRoute[0].AuthenticationOptions.ShouldBe(_ao); - _result[routeIndex].DownstreamRoute[0].ClaimsToHeaders.ShouldBe(_ctt); - _result[routeIndex].DownstreamRoute[0].ClaimsToQueries.ShouldBe(_ctt); - _result[routeIndex].DownstreamRoute[0].ClaimsToClaims.ShouldBe(_ctt); - _result[routeIndex].DownstreamRoute[0].QosOptions.ShouldBe(_qoso); - _result[routeIndex].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo); - _result[routeIndex].DownstreamRoute[0].CacheOptions.Region.ShouldBe(_cacheOptions.Region); - _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(0); - _result[routeIndex].DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_hho); - _result[routeIndex].DownstreamRoute[0].UpstreamHeadersFindAndReplace.ShouldBe(_ht.Upstream); - _result[routeIndex].DownstreamRoute[0].DownstreamHeadersFindAndReplace.ShouldBe(_ht.Downstream); - _result[routeIndex].DownstreamRoute[0].AddHeadersToUpstream.ShouldBe(_ht.AddHeadersToUpstream); - _result[routeIndex].DownstreamRoute[0].AddHeadersToDownstream.ShouldBe(_ht.AddHeadersToDownstream); - _result[routeIndex].DownstreamRoute[0].DownstreamAddresses.ShouldBe(_dhp); - _result[routeIndex].DownstreamRoute[0].LoadBalancerOptions.ShouldBe(_lbo); - _result[routeIndex].DownstreamRoute[0].UseServiceDiscovery.ShouldBe(_rro.UseServiceDiscovery); - _result[routeIndex].DownstreamRoute[0].DangerousAcceptAnyServerCertificateValidator.ShouldBe(expected.DangerousAcceptAnyServerCertificateValidator); - _result[routeIndex].DownstreamRoute[0].DelegatingHandlers.ShouldBe(expected.DelegatingHandlers); - _result[routeIndex].DownstreamRoute[0].ServiceName.ShouldBe(expected.ServiceName); - _result[routeIndex].DownstreamRoute[0].DownstreamScheme.ShouldBe(expected.DownstreamScheme); - _result[routeIndex].DownstreamRoute[0].RouteClaimsRequirement.ShouldBe(expected.RouteClaimsRequirement); - _result[routeIndex].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate); - _result[routeIndex].DownstreamRoute[0].Key.ShouldBe(expected.Key); + _result[routeIndex].DownstreamRoute[0].IsAuthenticated.ShouldBe(_rro.IsAuthenticated); + _result[routeIndex].DownstreamRoute[0].IsAuthorized.ShouldBe(_rro.IsAuthorized); + _result[routeIndex].DownstreamRoute[0].IsCached.ShouldBe(_rro.IsCached); + _result[routeIndex].DownstreamRoute[0].RequestIdKey.ShouldBe(_requestId); + _result[routeIndex].DownstreamRoute[0].LoadBalancerKey.ShouldBe(_rrk); + _result[routeIndex].DownstreamRoute[0].UpstreamPathTemplate.ShouldBe(_upt); + _result[routeIndex].DownstreamRoute[0].AuthenticationOptions.ShouldBe(_ao); + _result[routeIndex].DownstreamRoute[0].ClaimsToHeaders.ShouldBe(_ctt); + _result[routeIndex].DownstreamRoute[0].ClaimsToQueries.ShouldBe(_ctt); + _result[routeIndex].DownstreamRoute[0].ClaimsToClaims.ShouldBe(_ctt); + _result[routeIndex].DownstreamRoute[0].QosOptions.ShouldBe(_qoso); + _result[routeIndex].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo); + _result[routeIndex].DownstreamRoute[0].CacheOptions.Region.ShouldBe(_cacheOptions.Region); + _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(0); + _result[routeIndex].DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_hho); + _result[routeIndex].DownstreamRoute[0].UpstreamHeadersFindAndReplace.ShouldBe(_ht.Upstream); + _result[routeIndex].DownstreamRoute[0].DownstreamHeadersFindAndReplace.ShouldBe(_ht.Downstream); + _result[routeIndex].DownstreamRoute[0].AddHeadersToUpstream.ShouldBe(_ht.AddHeadersToUpstream); + _result[routeIndex].DownstreamRoute[0].AddHeadersToDownstream.ShouldBe(_ht.AddHeadersToDownstream); + _result[routeIndex].DownstreamRoute[0].DownstreamAddresses.ShouldBe(_dhp); + _result[routeIndex].DownstreamRoute[0].LoadBalancerOptions.ShouldBe(_lbo); + _result[routeIndex].DownstreamRoute[0].UseServiceDiscovery.ShouldBe(_rro.UseServiceDiscovery); + _result[routeIndex].DownstreamRoute[0].DangerousAcceptAnyServerCertificateValidator.ShouldBe(expected.DangerousAcceptAnyServerCertificateValidator); + _result[routeIndex].DownstreamRoute[0].DelegatingHandlers.ShouldBe(expected.DelegatingHandlers); + _result[routeIndex].DownstreamRoute[0].ServiceName.ShouldBe(expected.ServiceName); + _result[routeIndex].DownstreamRoute[0].DownstreamScheme.ShouldBe(expected.DownstreamScheme); + _result[routeIndex].DownstreamRoute[0].RouteClaimsRequirement.ShouldBe(expected.RouteClaimsRequirement); + _result[routeIndex].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate); + _result[routeIndex].DownstreamRoute[0].Key.ShouldBe(expected.Key); _result[routeIndex].DownstreamRoute[0].MetadataOptions.Metadata.ShouldBe(_expectedMetadata); - _result[routeIndex].UpstreamHttpMethod - .Select(x => x.Method) - .ToList() - .ShouldContain(x => x == expected.UpstreamHttpMethod[0]); - _result[routeIndex].UpstreamHttpMethod - .Select(x => x.Method) - .ToList() - .ShouldContain(x => x == expected.UpstreamHttpMethod[1]); - _result[routeIndex].UpstreamHost.ShouldBe(expected.UpstreamHost); - _result[routeIndex].DownstreamRoute.Count.ShouldBe(1); - _result[routeIndex].UpstreamTemplatePattern.ShouldBe(_upt); - _result[routeIndex].UpstreamHeaderTemplates.ShouldBe(_uht); - } - - private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguration globalConfig) - { - _rroCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); - _ridkCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); - _rrkCreator.Verify(x => x.Create(fileRoute), Times.Once); - _utpCreator.Verify(x => x.Create(fileRoute), Times.Exactly(2)); - _aoCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); - _cthCreator.Verify(x => x.Create(fileRoute.AddHeadersToRequest), Times.Once); - _cthCreator.Verify(x => x.Create(fileRoute.AddClaimsToRequest), Times.Once); - _cthCreator.Verify(x => x.Create(fileRoute.AddQueriesToRequest), Times.Once); - _qosoCreator.Verify(x => x.Create(fileRoute, globalConfig)); - _rloCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); - _coCreator.Verify(x => x.Create(fileRoute.FileCacheOptions, globalConfig, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod), Times.Once); - _hhoCreator.Verify(x => x.Create(fileRoute.HttpHandlerOptions), Times.Once); - _hfarCreator.Verify(x => x.Create(fileRoute), Times.Never); - _hfarCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); - _daCreator.Verify(x => x.Create(fileRoute), Times.Once); - _lboCreator.Verify(x => x.Create(fileRoute.LoadBalancerOptions), Times.Once); + _result[routeIndex].UpstreamHttpMethod.Count.ShouldBe(2); + _result[routeIndex].UpstreamHttpMethod.ShouldAllBe(actual => expected.UpstreamHttpMethod.Contains(actual.Method)); + _result[routeIndex].UpstreamHost.ShouldBe(expected.UpstreamHost); + _result[routeIndex].DownstreamRoute.Count.ShouldBe(1); + _result[routeIndex].UpstreamTemplatePattern.ShouldBe(_upt); + _result[routeIndex].UpstreamHeaderTemplates.ShouldBe(_uht); + } + + private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguration globalConfig) + { + _rroCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); + _ridkCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); + _rrkCreator.Verify(x => x.Create(fileRoute, It.IsAny()), Times.Once); + _utpCreator.Verify(x => x.Create(fileRoute), Times.Exactly(2)); + _aoCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); + _cthCreator.Verify(x => x.Create(fileRoute.AddHeadersToRequest), Times.Once); + _cthCreator.Verify(x => x.Create(fileRoute.AddClaimsToRequest), Times.Once); + _cthCreator.Verify(x => x.Create(fileRoute.AddQueriesToRequest), Times.Once); + _qosoCreator.Verify(x => x.Create(fileRoute, globalConfig)); + _rloCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); + _coCreator.Verify(x => x.Create(fileRoute.FileCacheOptions, globalConfig, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod), Times.Once); + _hhoCreator.Verify(x => x.Create(fileRoute.HttpHandlerOptions), Times.Once); + _hfarCreator.Verify(x => x.Create(fileRoute), Times.Never); + _hfarCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); + _daCreator.Verify(x => x.Create(fileRoute), Times.Once); + _lboCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions, globalConfig), Times.Once); - _metadataCreator.Verify(x => x.Create(fileRoute.Metadata, globalConfig), Times.Once); - } -} + _metadataCreator.Verify(x => x.Create(fileRoute.Metadata, globalConfig), Times.Once); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index 91f205caa..fa1e30b24 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -640,11 +640,12 @@ public async Task Configuration_is_valid_with_duplicate_routes_but_different_ups public async Task Configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is_not_set() { // Arrange - var route = GivenDefaultRoute() - .WithMethods() - .WithUpstreamHost("upstreamhost"); - var duplicate = Box(GivenDefaultRoute(null, "/www/test/")) - .Methods().Unbox(); + var route = GivenDefaultRoute(); + route.UpstreamHttpMethod.Clear(); + route.UpstreamHost = "upstreamhost"; + var duplicate = GivenDefaultRoute(null, "/www/test/"); + duplicate.UpstreamHttpMethod.Clear(); + duplicate.UpstreamHost = null; // ! GivenAConfiguration(route, duplicate); // Act diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index b11322afd..196c0424a 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -12,13 +12,16 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Ocelot.Configuration.Builder; using Ocelot.Configuration.Setter; using Ocelot.DependencyInjection; using Ocelot.Infrastructure; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Creators; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Multiplexer; using Ocelot.Requester; using Ocelot.Responses; +using Ocelot.ServiceDiscovery.Providers; using Ocelot.UnitTests.Requester; using Ocelot.Values; using System.Reflection; @@ -495,6 +498,17 @@ private void ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalanc creators.Count(c => c.GetType() == typeof(CookieStickySessionsCreator)).ShouldBe(1); creators.Count(c => c.GetType() == typeof(LeastConnectionCreator)).ShouldBe(1); creators.Count(c => c.GetType() == typeof(DelegateInvokingLoadBalancerCreator)).ShouldBe(1); + + // Call Create + var creator = creators.Single(c => c.GetType() == typeof(DelegateInvokingLoadBalancerCreator)); + Assert.NotNull(creator); + var route = new DownstreamRouteBuilder().Build(); + var provider = _serviceProvider.GetService(); + var response = creator.Create(route, provider); + Assert.NotNull(response); + Assert.False(response.IsError); + Assert.NotNull(response.Data); + Assert.IsType(response.Data); } private class FakeCustomLoadBalancer : ILoadBalancer diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs index 9a116a333..6fc18306c 100644 --- a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs @@ -38,18 +38,17 @@ public ClaimsToDownstreamPathMiddlewareTests() public async Task Should_call_add_queries_correctly() { // Arrange - var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() + var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToDownstreamPath(new List { new("UserId", "Subject", string.Empty, 0), }) .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + .Build(); + var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder( + new List(), + new Route(route, HttpMethod.Get)); // Arrange: Given The Down Stream Route Is _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DiscoveryDownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DiscoveryDownstreamRouteFinderTests.cs new file mode 100644 index 000000000..55ff15631 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DiscoveryDownstreamRouteFinderTests.cs @@ -0,0 +1,426 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.Infrastructure.Extensions; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.Responses; +using Ocelot.Values; +using System.Reflection; + +namespace Ocelot.UnitTests.DownstreamRouteFinder; + +public class DiscoveryDownstreamRouteFinderTests : UnitTest +{ + private readonly DiscoveryDownstreamRouteFinder _finder; + private QoSOptions _qoSOptions; + private readonly HttpHandlerOptions _handlerOptions; + private LoadBalancerOptions _loadBalancerOptions; + private Response _result; + private string _upstreamHost; + private string _upstreamUrlPath; + private string _upstreamHttpMethod; + private Dictionary _upstreamHeaders; + private IInternalConfiguration _configuration; + private Response _resultTwo; + private readonly string _upstreamQuery; + private readonly Mock _upstreamHeaderTemplatePatternCreator = new(); + private readonly MetadataOptions _metadataOptions; + private readonly RateLimitOptions _rateLimitOptions; + + public DiscoveryDownstreamRouteFinderTests() + { + _qoSOptions = new(new FileQoSOptions()); + _handlerOptions = new HttpHandlerOptionsBuilder().Build(); + _loadBalancerOptions = new(nameof(NoLoadBalancer), default, default); + _metadataOptions = new MetadataOptions(); + _rateLimitOptions = new RateLimitOptions(); + _finder = new(new RouteKeyCreator(), _upstreamHeaderTemplatePatternCreator.Object); + _upstreamQuery = string.Empty; + } + + [Fact] + public void Should_create_downstream_route() + { + // Arrange + GivenInternalConfiguration(); + GivenTheConfiguration(); + + // Act + WhenICreate(); + + // Assert + ThenTheDownstreamRouteIsCreated(); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "1229")] + public void Should_create_downstream_route_with_rate_limit_options() + { + // Arrange + var rateLimitOptions = new RateLimitOptions() + { + EnableRateLimiting = true, + ClientIdHeader = "test", + }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithServiceName("auth") + .WithRateLimitOptions(rateLimitOptions) + .WithLoadBalancerKey("|auth") + .WithLoadBalancerOptions(_loadBalancerOptions) + .WithQosOptions(_qoSOptions) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + var route = new Route(true, downstreamRoute); // create dynamic route + GivenInternalConfiguration(route); + GivenTheConfiguration(); + + // Act + WhenICreate(); + + // Assert + ThenTheDownstreamRouteIsCreated(lbKey: downstreamRoute.LoadBalancerKey); + + // Assert: With RateLimitOptions + var actual = _result.Data.Route.DownstreamRoute[0].RateLimitOptions; + actual.EnableRateLimiting.ShouldBeTrue(); + actual.EnableRateLimiting.ShouldBe(rateLimitOptions.EnableRateLimiting); + actual.ClientIdHeader.ShouldBe(rateLimitOptions.ClientIdHeader); + } + + [Fact] + public void Should_cache_downstream_route() + { + // Arrange + GivenInternalConfiguration(); + GivenTheConfiguration(); + _upstreamUrlPath = "/geoffisthebest/"; + + // Act + WhenICreate(); + GivenTheConfiguration(); + _upstreamUrlPath = "/geoffisthebest/"; + WhenICreateAgain(); + + // Assert + _result.ShouldBe(_resultTwo); + } + + [Fact] + public void Should_not_cache_downstream_route() + { + // Arrange + GivenInternalConfiguration(); + GivenTheConfiguration(); + _upstreamUrlPath = "/geoffistheworst/"; + + // Act + WhenICreate(); + GivenTheConfiguration(); + _upstreamUrlPath = "/geoffisthebest/"; + WhenICreateAgain(); + + // Assert + _result.ShouldNotBe(_resultTwo); + } + + [Fact] + public void Should_create_downstream_route_with_no_path() + { + // Arrange + GivenInternalConfiguration(); + GivenTheConfiguration(); + _upstreamUrlPath = "/auth/"; + + // Act + WhenICreate(); + + // Assert + ThenTheDownstreamPathIsForwardSlash(); + } + + [Fact] + public void Should_create_downstream_route_with_only_first_segment_no_traling_slash() + { + // Arrange + GivenInternalConfiguration(); + GivenTheConfiguration(); + _upstreamUrlPath = "/auth"; + + // Act + WhenICreate(); + + // Assert + ThenTheDownstreamPathIsForwardSlash(); + } + + [Fact] + public void Should_create_downstream_route_with_segments_no_traling_slash() + { + // Arrange + GivenInternalConfiguration(); + GivenTheConfiguration(); + _upstreamUrlPath = "/auth/test"; + + // Act + WhenICreate(); + + // Assert: Then the path does not have trailing slash + var actual = _result.Data.Route.DownstreamRoute[0]; + actual.DownstreamPathTemplate.Value.ShouldBe("/test"); + actual.ServiceName.ShouldBe("auth"); + actual.ServiceNamespace.ShouldBeEmpty(); + actual.LoadBalancerKey.ShouldBe(".auth"); + } + + [Fact] + [Trait("Feat", "351")] + [Trait("PR", "2324")] // This PR resolves the issue of forwarding the query string to the downstream when service discovery (dynamic routing), fixing a bug in the QoS Key construction for caching within the ResiliencePipelineRegistry. It now reuses the load balancing key to address the problem. + public void Should_create_downstream_route_and_forward_query_string() + { + // Arrange + GivenInternalConfiguration(); + GivenTheConfiguration(); + const string queryString = "?test=1&best=2"; + _upstreamUrlPath = "/auth/test" + queryString; + + // Act + WhenICreate(); + + // Assert: Then the query string is removed + var actual = _result.Data.Route.DownstreamRoute[0]; + actual.DownstreamPathTemplate.Value.ShouldContain(queryString); // !!! + actual.DownstreamPathTemplate.Value.ShouldBe("/test?test=1&best=2"); + actual.ServiceName.ShouldBe("auth"); + actual.ServiceNamespace.ShouldBeEmpty(); + actual.LoadBalancerKey.ShouldBe(".auth"); + } + + [Fact] + public void Should_create_downstream_route_for_sticky_sessions() + { + // Arrange + _loadBalancerOptions = new LoadBalancerOptions(nameof(CookieStickySessions), "boom", 1); + GivenInternalConfiguration(); + GivenTheConfiguration(); + + // Act + WhenICreate(); + + // Assert + var actual = _result.Data.Route.DownstreamRoute[0]; + actual.LoadBalancerKey.ShouldBe("CookieStickySessions:boom"); + actual.LoadBalancerOptions.Type.ShouldBe("CookieStickySessions"); + actual.LoadBalancerOptions.ShouldBe(_loadBalancerOptions); + } + + [Fact] + public void Should_create_downstream_route_with_qos() + { + // Arrange + _qoSOptions = new QoSOptionsBuilder() + .WithExceptionsAllowedBeforeBreaking(1) + .WithTimeoutValue(1) + .Build(); + GivenInternalConfiguration(); + GivenTheConfiguration(); + + // Act + WhenICreate(); + + // Assert: Then the Qos options are set + var actual = _result.Data.Route.DownstreamRoute[0]; + actual.QosOptions.ShouldNotBeNull(); + actual.QosOptions.UseQos.ShouldBeTrue(); + } + + [Fact] + public void Should_create_downstream_route_with_handler_options() + { + // Arrange + GivenInternalConfiguration(); + GivenTheConfiguration(); + + // Act + WhenICreate(); + + // Assert: Then The Handler Options Are Set + _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); + } + + [Theory] + [Trait("PR", "2324")] + [InlineData("/service1", "service1", "")] + [InlineData("/service2/", "service2", "")] + [InlineData("/service3/bla", "service3", "")] + [InlineData("/namespace1.service1", "service1", "namespace1")] + [InlineData("/namespace2.service2/", "service2", "namespace2")] + [InlineData("/namespace3.service3/bla-bla", "service3", "namespace3")] + [InlineData("/namespace4.service.4/ha-a", "service.4", "namespace4")] + [InlineData("/name.space5.service5/ha-ha", "space5.service5", "name")] + public void GetServiceName(string urlPath, string expected, string expectedNamespace) + { + var method = _finder.GetType().GetMethod(nameof(GetServiceName), BindingFlags.Instance | BindingFlags.NonPublic); + object[] parameters = [urlPath, null]; + + // Act + string actual = (string)method.Invoke(_finder, parameters); + string actualNamespace = (string)parameters[1]; + + Assert.Equal(expected, actual); + Assert.Equal(expectedNamespace, actualNamespace); + } + + [Fact] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + public void Should_create_downstream_route_with_load_balancer_options() + { + // Arrange + var lbOptions = new LoadBalancerOptions("testBalancer", "testKey", 3); + var downstreamRoute = new DownstreamRouteBuilder() + .WithServiceName("auth") + .WithLoadBalancerOptions(lbOptions) + .WithLoadBalancerKey("|auth") + .WithMetadata(_metadataOptions) + .WithRateLimitOptions(_rateLimitOptions) + .WithQosOptions(_qoSOptions) + .WithDownstreamScheme("http") + .Build(); + var route = new Route(true, downstreamRoute); // create dynamic route + GivenInternalConfiguration(route); + GivenTheConfiguration(); + + // Act + WhenICreate(); + + // Assert + ThenTheDownstreamRouteIsCreated(lbType: "testBalancer", lbKey: "|auth"); + var downstream = _result.Data.Route.DownstreamRoute[0]; + downstream.LoadBalancerOptions.ShouldNotBeNull(); + downstream.LoadBalancerOptions.Type.ShouldBe("testBalancer"); + downstream.LoadBalancerOptions.Key.ShouldBe("testKey"); + downstream.LoadBalancerOptions.ExpiryInMs.ShouldBe(3); + } + + [Theory] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + [InlineData(false)] + [InlineData(true)] + public void ShouldFindFirstOrDefaultDownstreamRoute_WithOrWithoutServiceNamespace(bool hasNamespace) + { + // Arrange + var lbOptions = new LoadBalancerOptions("testBalancer", "testKey", 3); + var dRoute1 = new DownstreamRouteBuilder() + .WithServiceName("service1") + .Build(); + var dRoute2 = new DownstreamRouteBuilder() + .WithServiceName("service2") + .WithServiceNamespace(hasNamespace ? "namespace2" : string.Empty) + .WithLoadBalancerKey("namespace2-service2") + .WithLoadBalancerOptions(lbOptions) + .WithQosOptions(_qoSOptions) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + var route = new Route(true) + { + DownstreamRoute = [dRoute1, dRoute2], + }; + GivenInternalConfiguration(route, 1); + GivenTheConfiguration(); + _upstreamUrlPath = hasNamespace + ? $"/{dRoute2.ServiceNamespace}.{dRoute2.ServiceName}/test" + : $"/{dRoute2.ServiceName}/test"; + + // Act + WhenICreate(); + + // Assert + ThenTheDownstreamRouteIsCreated("service2", hasNamespace ? "namespace2" : "", "testBalancer", "namespace2-service2"); + var downstream = _result.Data.Route.DownstreamRoute[0]; + downstream.LoadBalancerOptions.ShouldNotBeNull(); + downstream.LoadBalancerOptions.Type.ShouldBe("testBalancer"); + downstream.LoadBalancerOptions.Key.ShouldBe("testKey"); + downstream.LoadBalancerOptions.ExpiryInMs.ShouldBe(3); + } + + private void ThenTheDownstreamRouteIsCreated(string serviceName = null, string serviceNamespace = null, string lbType = null, string lbKey = null) + { + _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); + _result.Data.Route.UpstreamHttpMethod.ShouldContain(HttpMethod.Get); + _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe(serviceName ?? "auth"); + _result.Data.Route.DownstreamRoute[0].ServiceNamespace.ShouldBe(serviceNamespace ?? string.Empty); + _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe(lbKey ?? ".auth"); + _result.Data.Route.DownstreamRoute[0].UseServiceDiscovery.ShouldBeTrue(); + _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldNotBeNull(); + _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull(); + _result.Data.Route.DownstreamRoute[0].DownstreamScheme.ShouldBe("http"); + _result.Data.Route.DownstreamRoute[0].LoadBalancerOptions.Type.ShouldBe(lbType ?? nameof(NoLoadBalancer)); + _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); + _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull(); + _result.Data.Route.UpstreamTemplatePattern.ShouldNotBeNull(); + _result.Data.Route.DownstreamRoute[0].UpstreamPathTemplate.ShouldNotBeNull(); + var kv = _upstreamHeaders.First(); + _result.Data.Route.UpstreamHeaderTemplates.ShouldNotBeNull() + .FirstOrDefault(x => x.Key == kv.Key).Value.Template.ShouldBe(kv.Value); + _result.Data.Route.DownstreamRoute[0].UpstreamHeaders.ShouldNotBeNull() + .FirstOrDefault(x => x.Key == kv.Key).Value.Template.ShouldBe(kv.Value); + } + + private void ThenTheDownstreamPathIsForwardSlash() + { + _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/"); + _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); + _result.Data.Route.DownstreamRoute[0].ServiceNamespace.ShouldBeEmpty(); + _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe(".auth"); + } + + private void GivenTheConfiguration() + { + _upstreamHost = "doesnt matter"; + _upstreamUrlPath = "/auth/test"; + _upstreamHttpMethod = "GET"; + _upstreamHeaders = new() + { + { "testHeader", "testHeaderValue" }, + }; + var kv = _upstreamHeaders.First(); + _upstreamHeaderTemplatePatternCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) + .Returns(new Dictionary() + { + { kv.Key, new(kv.Value, kv.Value) }, + }); + } + + private void GivenInternalConfiguration(Route route = null, int index = 0) + { + var dr = route?.DownstreamRoute[index]; + _configuration = new InternalConfiguration( + route is null ? null : new() { route }, + "/AdminPath", + null, + "requestID", + dr?.LoadBalancerOptions ?? _loadBalancerOptions, + (dr?.DownstreamScheme).IfEmpty(Uri.UriSchemeHttp), + dr?.QosOptions ?? _qoSOptions, + dr?.HttpHandlerOptions ?? _handlerOptions, + dr?.DownstreamHttpVersion ?? new Version("1.1"), + dr?.DownstreamHttpVersionPolicy ?? HttpVersionPolicy.RequestVersionOrLower, + dr?.MetadataOptions ?? _metadataOptions, + dr?.RateLimitOptions ?? _rateLimitOptions, + dr?.Timeout ?? 111); + } + + private void WhenICreate() + { + _result = _finder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); + } + + private void WhenICreateAgain() + { + _resultTwo = _finder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs deleted file mode 100644 index fe4927b6f..000000000 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs +++ /dev/null @@ -1,426 +0,0 @@ -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Ocelot.DownstreamRouteFinder.Finder; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Responses; -using Ocelot.Values; - -namespace Ocelot.UnitTests.DownstreamRouteFinder; - -public class DownstreamRouteCreatorTests : UnitTest -{ - private readonly DownstreamRouteCreator _creator; - private readonly QoSOptions _qoSOptions; - private readonly HttpHandlerOptions _handlerOptions; - private readonly LoadBalancerOptions _loadBalancerOptions; - private Response _result; - private string _upstreamHost; - private string _upstreamUrlPath; - private string _upstreamHttpMethod; - private Dictionary _upstreamHeaders; - private IInternalConfiguration _configuration; - private Response _resultTwo; - private readonly string _upstreamQuery; - private readonly Mock _upstreamHeaderTemplatePatternCreator; - - public DownstreamRouteCreatorTests() - { - _qoSOptions = new(new FileQoSOptions()); - _handlerOptions = new HttpHandlerOptionsBuilder().Build(); - _loadBalancerOptions = new LoadBalancerOptionsBuilder().WithType(nameof(NoLoadBalancer)).Build(); - _upstreamHeaderTemplatePatternCreator = new(); - _creator = new DownstreamRouteCreator(_upstreamHeaderTemplatePatternCreator.Object); - _upstreamQuery = string.Empty; - } - - [Fact] - public void Should_create_downstream_route() - { - // Arrange - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration); - - // Act - WhenICreate(); - - // Assert - ThenTheDownstreamRouteIsCreated(); - } - - [Fact] - public void Should_create_downstream_route_with_rate_limit_options() - { - // Arrange - var rateLimitOptions = new RateLimitOptions() - { - EnableRateLimiting = true, - ClientIdHeader = "test", - }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithServiceName("auth") - .WithRateLimitOptions(rateLimitOptions) - .Build(); - var route = new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build(); - var routes = new List { route }; - var configuration = new InternalConfiguration( - routes, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration); - - // Act - WhenICreate(); - - // Assert - ThenTheDownstreamRouteIsCreated(); - WithRateLimitOptions(rateLimitOptions); - } - - [Fact] - public void Should_cache_downstream_route() - { - // Arrange - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration, "/geoffisthebest/"); - - // Act - WhenICreate(); - GivenTheConfiguration(configuration, "/geoffisthebest/"); - WhenICreateAgain(); - - // Assert - _result.ShouldBe(_resultTwo); - } - - [Fact] - public void Should_not_cache_downstream_route() - { - // Arrange - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration, "/geoffistheworst/"); - - // Act - WhenICreate(); - GivenTheConfiguration(configuration, "/geoffisthebest/"); - WhenICreateAgain(); - - // Assert - _result.ShouldNotBe(_resultTwo); - } - - [Fact] - public void Should_create_downstream_route_with_no_path() - { - // Arrange - var upstreamUrlPath = "/auth/"; - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration, upstreamUrlPath); - - // Act - WhenICreate(); - - // Assert - ThenTheDownstreamPathIsForwardSlash(); - } - - [Fact] - public void Should_create_downstream_route_with_only_first_segment_no_traling_slash() - { - // Arrange - var upstreamUrlPath = "/auth"; - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration, upstreamUrlPath); - - // Act - WhenICreate(); - - // Assert - ThenTheDownstreamPathIsForwardSlash(); - } - - [Fact] - public void Should_create_downstream_route_with_segments_no_traling_slash() - { - // Arrange - var upstreamUrlPath = "/auth/test"; - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrHigher); - GivenTheConfiguration(configuration, upstreamUrlPath); - - // Act - WhenICreate(); - - // Assert - ThenThePathDoesNotHaveTrailingSlash(); - } - - [Fact] - public void Should_create_downstream_route_and_remove_query_string() - { - // Arrange - var upstreamUrlPath = "/auth/test?test=1&best=2"; - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration, upstreamUrlPath); - - // Act - WhenICreate(); - - // Assert - ThenTheQueryStringIsRemoved(); - } - - [Fact] - public void Should_create_downstream_route_for_sticky_sessions() - { - // Arrange - var loadBalancerOptions = new LoadBalancerOptionsBuilder().WithType(nameof(CookieStickySessions)).WithKey("boom").WithExpiryInMs(1).Build(); - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration); - - // Act - WhenICreate(); - - // Assert - ThenTheStickySessionLoadBalancerIsUsed(loadBalancerOptions); - } - - [Fact] - public void Should_create_downstream_route_with_qos() - { - // Arrange - var qoSOptions = new QoSOptionsBuilder() - .WithExceptionsAllowedBeforeBreaking(1) - .WithTimeoutValue(1) - .WithKey("/auth/test|GET") - .Build(); - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration); - - // Act - WhenICreate(); - - // Assert - ThenTheQosOptionsAreSet(qoSOptions); - } - - [Fact] - public void Should_create_downstream_route_with_handler_options() - { - // Arrange - var configuration = new InternalConfiguration( - null, - "doesnt matter", - null, - "doesnt matter", - _loadBalancerOptions, - "http", - _qoSOptions, - _handlerOptions, - new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); - GivenTheConfiguration(configuration); - - // Act - WhenICreate(); - - // Assert: Then The Handler Options Are Set - _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); - } - - private void WithRateLimitOptions(RateLimitOptions expected) - { - _result.Data.Route.DownstreamRoute[0].RateLimitOptions.EnableRateLimiting.ShouldBeTrue(); - _result.Data.Route.DownstreamRoute[0].RateLimitOptions.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting); - _result.Data.Route.DownstreamRoute[0].RateLimitOptions.ClientIdHeader.ShouldBe(expected.ClientIdHeader); - } - - private void ThenTheDownstreamRouteIsCreated() - { - _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); - _result.Data.Route.UpstreamHttpMethod[0].ShouldBe(HttpMethod.Get); - _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); - _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe("/auth/test|GET"); - _result.Data.Route.DownstreamRoute[0].UseServiceDiscovery.ShouldBeTrue(); - _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldNotBeNull(); - _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull(); - _result.Data.Route.DownstreamRoute[0].DownstreamScheme.ShouldBe("http"); - _result.Data.Route.DownstreamRoute[0].LoadBalancerOptions.Type.ShouldBe(nameof(NoLoadBalancer)); - _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); - _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull().Key.ShouldBe("/auth/test|GET"); - _result.Data.Route.UpstreamTemplatePattern.ShouldNotBeNull(); - _result.Data.Route.DownstreamRoute[0].UpstreamPathTemplate.ShouldNotBeNull(); - var kv = _upstreamHeaders.First(); - _result.Data.Route.UpstreamHeaderTemplates.ShouldNotBeNull() - .FirstOrDefault(x => x.Key == kv.Key).Value.Template.ShouldBe(kv.Value); - _result.Data.Route.DownstreamRoute[0].UpstreamHeaders.ShouldNotBeNull() - .FirstOrDefault(x => x.Key == kv.Key).Value.Template.ShouldBe(kv.Value); - } - - private void ThenTheDownstreamPathIsForwardSlash() - { - _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/"); - _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); - _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe("/auth/|GET"); - } - - private void ThenThePathDoesNotHaveTrailingSlash() - { - _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); - _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); - _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe("/auth/test|GET"); - } - - private void ThenTheQueryStringIsRemoved() - { - _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); - _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); - _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe("/auth/test|GET"); - } - - private void ThenTheStickySessionLoadBalancerIsUsed(LoadBalancerOptions expected) - { - _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe($"{nameof(CookieStickySessions)}:boom"); - _result.Data.Route.DownstreamRoute[0].LoadBalancerOptions.Type.ShouldBe(nameof(CookieStickySessions)); - _result.Data.Route.DownstreamRoute[0].LoadBalancerOptions.ShouldBe(expected); - } - - private void ThenTheQosOptionsAreSet(QoSOptions expected) - { - _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull().Key.ShouldBe(expected.Key); - _result.Data.Route.DownstreamRoute[0].QosOptions.UseQos.ShouldBeTrue(); - } - - private void GivenTheConfiguration(IInternalConfiguration config) - { - _upstreamHost = "doesnt matter"; - _upstreamUrlPath = "/auth/test"; - _upstreamHttpMethod = "GET"; - _upstreamHeaders = new() - { - { "testHeader", "testHeaderValue" }, - }; - var kv = _upstreamHeaders.First(); - _configuration = config; - _upstreamHeaderTemplatePatternCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) - .Returns(new Dictionary() - { - { kv.Key, new(kv.Value, kv.Value) }, - }); - } - - private void GivenTheConfiguration(IInternalConfiguration config, string upstreamUrlPath) - { - GivenTheConfiguration(config); - _upstreamUrlPath = upstreamUrlPath; - } - - private void WhenICreate() - { - _result = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); - } - - private void WhenICreateAgain() - { - _resultTwo = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); - } -} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index 44417aa4b..f99f57ce1 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -45,22 +45,20 @@ public async Task Should_call_scoped_data_repository_correctly() null, new ServiceProviderConfigurationBuilder().Build(), string.Empty, - new LoadBalancerOptionsBuilder().Build(), + new LoadBalancerOptions(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); + HttpVersionPolicy.RequestVersionOrLower, + default, default, default); var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithUpstreamHttpMethod(new List { "Get" }) .Build(); GivenTheDownStreamRouteFinderReturns(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build())); + new Route(downstreamRoute, HttpMethod.Get))); GivenTheFollowingConfig(config); // Act diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index 4f51827c7..b1239aa6b 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -1,4 +1,4 @@ -using Consul; +using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; @@ -45,26 +45,11 @@ public void Should_return_highest_priority_when_first() _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); + var expectedRoute = GivenRoute(method: "Post", priority: 1); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .Build(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) - .Build(), + expectedRoute, + GivenRoute(method: "Post", priority: 0), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -77,16 +62,7 @@ public void Should_return_highest_priority_when_first() // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .WithUpstreamHttpMethod(new List { "Post" }) - .Build()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .WithUpstreamHttpMethod(new List { "Post" }) - .Build() - )); + expectedRoute)); } [Fact] @@ -98,26 +74,11 @@ public void Should_return_highest_priority_when_lowest() _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); + var expectedRoute = GivenRoute(method: "Post", priority: 1); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) - .Build(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(method: "Post", priority: 0), + expectedRoute, }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -130,16 +91,7 @@ public void Should_return_highest_priority_when_lowest() // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .WithUpstreamHttpMethod(new List { "Post" }) - .Build()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .WithUpstreamHttpMethod(new List { "Post" }) - .Build() - )); + expectedRoute)); } [Fact] @@ -153,15 +105,7 @@ public void Should_return_route() GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -174,16 +118,7 @@ public void Should_return_route() // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - )); + GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(); } @@ -199,15 +134,7 @@ public void Should_not_append_slash_to_upstream_url_path() GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -220,16 +147,7 @@ public void Should_not_append_slash_to_upstream_url_path() // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - )); + GivenRoute(priority: 1))); // Assert: Then The Url Matcher Is Called Correctly _mockUrlMatcher.Verify(x => x.Match("matchInUrlMatcher", _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once); @@ -246,15 +164,7 @@ public void Should_return_route_if_upstream_path_and_upstream_template_are_the_s GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -267,16 +177,7 @@ public void Should_return_route_if_upstream_path_and_upstream_template_are_the_s // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - )); + GivenRoute(priority: 1))); } [Fact] @@ -290,24 +191,8 @@ public void Should_return_correct_route_for_http_verb() GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPathForAPost") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(downstream: "someDownstreamPath", method: "Get", priority: 1), + GivenRoute(downstream: "someDownstreamPathForAPost", method: "Post", priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -320,16 +205,8 @@ public void Should_return_correct_route_for_http_verb() // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPathForAPost") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build() - )); + GivenRoute(downstream: "someDownstreamPathForAPost", method: "Post", priority: 1) + )); } [Fact] @@ -341,15 +218,7 @@ public void Should_not_return_route() _upstreamQuery = string.Empty; _routesConfig = new List { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("somPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("somePath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("somePath", 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(downstream: "somPath", upstream: "somePath", priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(false)); @@ -375,15 +244,7 @@ public void Should_return_correct_route_for_http_verb_setting_multiple_upstream_ GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get", "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get", "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(upstreamMethods: ["Get", "Post"], priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -396,16 +257,7 @@ public void Should_return_correct_route_for_http_verb_setting_multiple_upstream_ // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build() - )); + GivenRoute(upstreamMethods: ["Post"], priority: 1))); } [Fact] @@ -419,15 +271,7 @@ public void Should_return_correct_route_for_http_verb_setting_all_upstream_http_ GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(upstreamMethods: [], priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -440,16 +284,7 @@ public void Should_return_correct_route_for_http_verb_setting_all_upstream_http_ // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build() - )); + GivenRoute(upstreamMethods: ["Post"], priority: 1))); } [Fact] @@ -463,15 +298,7 @@ public void Should_not_return_route_for_http_verb_not_setting_in_upstream_http_m GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get", "Patch", "Delete" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get", "Patch", "Delete" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(upstreamMethods: ["Get", "Patch", "Delete"], priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -498,16 +325,7 @@ public void Should_return_route_when_host_matches() GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build(), + GivenRoute(host: "MATCH", priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -520,16 +338,7 @@ public void Should_return_route_when_host_matches() // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - )); + GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(); } @@ -545,15 +354,7 @@ public void Should_return_route_when_upstreamhost_is_null() GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build(), + GivenRoute(host: null, priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -566,16 +367,7 @@ public void Should_return_route_when_upstreamhost_is_null() // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - )); + GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(); } @@ -591,26 +383,8 @@ public void Should_not_return_route_when_host_doesnt_match() GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List()) // empty list of methods - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List()) // empty list of methods - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build(), + GivenRoute(host: "MATCH", upstreamMethods: ["Get"], priority: 1), + GivenRoute(host: "MATCH", upstreamMethods: [], priority: 1), // empty list of methods }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -637,16 +411,7 @@ public void Should_not_return_route_when_host_doesnt_match_with_empty_upstream_h GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build(), + GivenRoute(host: "MATCH", upstreamMethods: [], priority: 1), // empty list of methods }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -673,16 +438,7 @@ public void Should_return_route_when_host_does_match_with_empty_upstream_http_me GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build(), + GivenRoute(host: "MATCH", upstreamMethods: [], priority: 1), // empty list of methods }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -708,25 +464,8 @@ public void Should_return_route_when_host_matches_but_null_host_on_same_path_fir GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("THENULLPATH") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build(), + GivenRoute(downstream: "THENULLPATH", priority: 1), + GivenRoute(host: "MATCH", priority: 1), // empty list of methods }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -739,16 +478,7 @@ public void Should_return_route_when_host_matches_but_null_host_on_same_path_fir // Assert ThenTheFollowingIsReturned(new( new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "test")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "test")) - .Build() - )); + GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(1, 0); ThenTheUrlMatcherIsCalledCorrectly(1, 1); } @@ -780,16 +510,7 @@ public void Should_return_route_when_upstream_headers_match() GivenTheHeaderPlaceholderAndNameFinderReturns(headerPlaceholders); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new() {"Get"}) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new() {"Get"}) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHeaders(upstreamHeadersConfig) - .Build(), + GivenRoute(headers: upstreamHeadersConfig, priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -801,17 +522,8 @@ public void Should_return_route_when_upstream_headers_match() // Assert ThenTheFollowingIsReturned(new( - urlPlaceholders.Union(headerPlaceholders).ToList(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new() { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new() { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - )); + urlPlaceholders.Union(headerPlaceholders).ToList(), + GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(); } @@ -834,26 +546,8 @@ public void Should_not_return_route_when_upstream_headers_dont_match() GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new() { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new() { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHeaders(upstreamHeadersConfig) - .Build(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new() { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new() { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHeaders(upstreamHeadersConfig) - .Build(), + GivenRoute(headers: upstreamHeadersConfig, priority: 1), + GivenRoute(headers: upstreamHeadersConfig, priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); @@ -867,6 +561,68 @@ public void Should_not_return_route_when_upstream_headers_dont_match() _result.IsError.ShouldBeTrue(); } + [Theory] + [Trait("Feat", "585")] + [Trait("Feat", "2319")] // https://github.com/ThreeMammals/Ocelot/pull/2324 + [InlineData(false)] + [InlineData(true)] + public void Should_filter_static_routes(bool isDynamic) + { + // Arrange + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + _upstreamUrlPath = "matchInUrlMatcher/"; + _upstreamQuery = string.Empty; + GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); + GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); + _routesConfig = new() + { + GivenRoute(priority: 1), + GivenRoute(isDynamic: isDynamic, priority: 1), + }; + GivenTheConfigurationIs(string.Empty, serviceProviderConfig); + GivenTheUrlMatcherReturns(new UrlMatch(true)); + GivenTheHeadersMatcherReturns(true); + _upstreamHttpMethod = "Get"; + + // Act, Assert + _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); + _result.Data.Route.IsDynamic.ShouldBeFalse(); + + // Act, Assert 2 + _routesConfig.RemoveAll(r => !r.IsDynamic); // remove all static routes + _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); + _result.IsError.ShouldBeTrue(); + } + + private static Route GivenRoute(bool? isDynamic = null, string downstream = null, + List upstreamMethods = null, string method = null, + UpstreamPathTemplate upTemplate = null, string upstream = null, int? priority = null, + string host = null, + IDictionary headers = null) + { + var route = GivenDownstreamRoute(downstream, upstreamMethods, method, upTemplate, upstream, priority); + upstream ??= "someUpstreamPath"; + upTemplate ??= new(upstream, priority ?? 1, false, upstream); + upstreamMethods ??= [method ?? HttpMethods.Get]; + return new(isDynamic ?? false) + { + DownstreamRoute = [route], + UpstreamHttpMethod = upstreamMethods.Select(m => new HttpMethod(m)).ToHashSet(), + UpstreamTemplatePattern = upTemplate, + UpstreamHost = host, + UpstreamHeaderTemplates = headers, + }; + } + + private static DownstreamRoute GivenDownstreamRoute(string downstream = null, + List upstreamMethods = null, string method = null, + UpstreamPathTemplate upTemplate = null, string upstream = null, int? priority = null) + => new DownstreamRouteBuilder() + .WithDownstreamPathTemplate(downstream ?? "someDownstreamPath") + .WithUpstreamHttpMethod(upstreamMethods ?? [method ?? HttpMethods.Get]) + .WithUpstreamPathTemplate(upTemplate ?? new(upstream ?? "someUpstreamPath", priority ?? 1, false, upstream ?? "someUpstreamPath")) + .Build(); + private void GivenTheTemplateVariableAndNameFinderReturns(Response> response) { _urlPlaceholderFinder @@ -921,12 +677,13 @@ private void GivenTheConfigurationIs(string adminPath, ServiceProviderConfigurat adminPath, serviceProviderConfig, string.Empty, - new LoadBalancerOptionsBuilder().Build(), + new LoadBalancerOptions(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); + HttpVersionPolicy.RequestVersionOrLower, + default, default, default); } private void ThenTheFollowingIsReturned(DownstreamRouteHolder expected) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteHolderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteHolderTests.cs new file mode 100644 index 000000000..9475ceb38 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteHolderTests.cs @@ -0,0 +1,33 @@ +using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; + +namespace Ocelot.UnitTests.DownstreamRouteFinder; + +public class DownstreamRouteHolderTests +{ + [Fact] + public void Ctor() + { + // Arrange, Act + DownstreamRouteHolder holder = new(); + + Assert.Null(holder.Route); + Assert.Null(holder.TemplatePlaceholderNameAndValues); + } + + [Fact] + public void Ctor_List_Route() + { + // Arrange + Route route = new(); + List placeholders = new(); + + // Act + DownstreamRouteHolder holder = new(placeholders, route); + + // Assert + Assert.Equal(route, holder.Route); + Assert.Equal(placeholders, holder.TemplatePlaceholderNameAndValues); + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs index a009ea29d..2050a0237 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs @@ -27,8 +27,9 @@ public DownstreamRouteProviderFactoryTests() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); Features.AddHeaderRouting(services); // AddSingleton() var provider = services.BuildServiceProvider(true); _logger = new Mock(); @@ -41,7 +42,7 @@ public DownstreamRouteProviderFactoryTests() public void Should_return_downstream_route_finder() { // Arrange - var route = new RouteBuilder().Build(); + var route = new Route(); GivenTheRoutes(route); // Act @@ -55,9 +56,10 @@ public void Should_return_downstream_route_finder() public void Should_return_downstream_route_finder_when_not_dynamic_re_route_and_service_discovery_on() { // Arrange - var route = new RouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("woot").Build()) - .Build(); + var route = new Route() + { + UpstreamTemplatePattern = new UpstreamPathTemplateBuilder().WithOriginalValue("woot").Build(), + }; var spConfig = new ServiceProviderConfigurationBuilder() .WithScheme("http").WithHost("test").WithPort(50).WithType("test").Build(); GivenTheRoutes(route, spConfig); @@ -141,14 +143,14 @@ public void Should_return_downstream_route_creator() _result = _factory.Get(_config); // Assert - _result.ShouldBeOfType(); + _result.ShouldBeOfType(); } [Fact] public void Should_return_downstream_route_creator_with_dynamic_re_route() { // Arrange - var route = new RouteBuilder().Build(); + var route = new Route(); var spConfig = new ServiceProviderConfigurationBuilder() .WithScheme("http").WithHost("test").WithPort(50).WithType("test").Build(); GivenTheRoutes(route, spConfig); @@ -157,7 +159,7 @@ public void Should_return_downstream_route_creator_with_dynamic_re_route() _result = _factory.Get(_config); // Assert - _result.ShouldBeOfType(); + _result.ShouldBeOfType(); } private void GivenTheRoutes(Route route, ServiceProviderConfiguration config = null) @@ -167,11 +169,12 @@ private void GivenTheRoutes(Route route, ServiceProviderConfiguration config = n string.Empty, config, string.Empty, - new LoadBalancerOptionsBuilder().Build(), + new LoadBalancerOptions(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1"), - HttpVersionPolicy.RequestVersionOrLower); + HttpVersionPolicy.RequestVersionOrLower, + default, default, default); } } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs index e0a3bc2de..67a2ba0bb 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; @@ -14,13 +16,8 @@ public void Can_replace_no_template_variables() { // Arrange var holder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + new List(), + GivenRoute()); // Act var result = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); @@ -34,14 +31,8 @@ public void Can_replace_no_template_variables_with_slash() { // Arrange var holder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + new List(), + GivenRoute("/")); // Act var result = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); @@ -54,14 +45,9 @@ public void Can_replace_no_template_variables_with_slash() public void Can_replace_url_no_slash() { // Arrange - var holder = new DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("api") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + var holder = new DownstreamRouteHolder( + new List(), + GivenRoute("api")); // Act var result = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); @@ -74,14 +60,9 @@ public void Can_replace_url_no_slash() public void Can_replace_url_one_slash() { // Arrange - var holder = new DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("api/") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + var holder = new DownstreamRouteHolder( + new List(), + GivenRoute("api/")); // Act var result = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); @@ -94,14 +75,9 @@ public void Can_replace_url_one_slash() public void Can_replace_url_multiple_slash() { // Arrange - var holder = new DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("api/product/products/") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + var holder = new DownstreamRouteHolder( + new List(), + GivenRoute("api/product/products/")); // Act var result = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); @@ -118,14 +94,9 @@ public void Can_replace_url_one_template_variable() { new("{productId}", "1"), }; - var holder = new DownstreamRouteHolder(templateVariables, - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("productservice/products/{productId}/") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + var holder = new DownstreamRouteHolder( + templateVariables, + GivenRoute("productservice/products/{productId}/")); // Act var result = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); @@ -142,14 +113,9 @@ public void Can_replace_url_one_template_variable_with_path_after() { new("{productId}", "1"), }; - var holder = new DownstreamRouteHolder(templateVariables, - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("productservice/products/{productId}/variants") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + var holder = new DownstreamRouteHolder( + templateVariables, + GivenRoute("productservice/products/{productId}/variants")); // Act var result = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); @@ -167,14 +133,9 @@ public void Can_replace_url_two_template_variable() new("{productId}", "1"), new("{variantId}", "12"), }; - var holder = new DownstreamRouteHolder(templateVariables, - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("productservice/products/{productId}/variants/{variantId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + var holder = new DownstreamRouteHolder( + templateVariables, + GivenRoute("productservice/products/{productId}/variants/{variantId}")); // Act var result = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); @@ -193,14 +154,9 @@ public void Can_replace_url_three_template_variable() new("{variantId}", "12"), new("{categoryId}", "34"), }; - var holder = new DownstreamRouteHolder(templateVariables, - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("productservice/category/{categoryId}/products/{productId}/variants/{variantId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + var holder = new DownstreamRouteHolder( + templateVariables, + GivenRoute("productservice/category/{categoryId}/products/{productId}/variants/{variantId}")); // Act var result = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); @@ -208,4 +164,20 @@ public void Can_replace_url_three_template_variable() // Assert result.Data.Value.ShouldBe("productservice/category/34/products/1/variants/12"); } + + private static Route GivenRoute(string downstream = null, string method = null) + { + var route = GivenDownstreamRoute(downstream, method); + return new() + { + DownstreamRoute = [route], + UpstreamHttpMethod = [method is null ? HttpMethod.Get : new(method)], + }; + } + + private static DownstreamRoute GivenDownstreamRoute(string downstream = null, string method = null) + => new DownstreamRouteBuilder() + .WithDownstreamPathTemplate(downstream) + .WithUpstreamHttpMethod([method ?? HttpMethods.Get]) + .Build(); } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index 77ec64b5e..174224171 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -5,7 +5,6 @@ using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamUrlCreator; using Ocelot.DownstreamUrlCreator.Middleware; -using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; @@ -52,11 +51,7 @@ public async Task Should_replace_scheme_and_path() .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - )); + new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123"); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("/api/products/1"); @@ -87,11 +82,7 @@ public async Task Should_replace_query_string() new("{subscriptionId}", "1"), new("{unitId}", "2"), }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - )); + new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2"); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); @@ -122,11 +113,7 @@ public async Task Should_replace_query_string_but_leave_non_placeholder_queries( new("{subscriptionId}", "1"), new("{unitId}", "2"), }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - )); + new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2&productId=2"); // unitId is the first GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); @@ -157,11 +144,7 @@ public async Task Should_replace_query_string_but_leave_non_placeholder_queries_ new("{subscriptionId}", "1"), new("{unitId}", "2"), }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - )); + new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?productId=2&unitId=2"); // unitId is the second GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); @@ -193,11 +176,7 @@ public async Task Should_replace_query_string_exact_match() new("{unitId}", "2"), new("{unitIdIty}", "3"), }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - )); + new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2?unitIdIty=3"); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("api/units/1/2/updates/3"); @@ -226,11 +205,7 @@ public async Task Should_not_create_service_fabric_url() .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - )); + new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123"); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("/api/products/1"); @@ -253,7 +228,7 @@ public async Task Should_create_service_fabric_url() .Build(); var downstreamRouteHolder = new DownstreamRouteHolder( new List(), - new RouteBuilder().WithDownstreamRoute(downstreamRoute).Build()); + new Route(downstreamRoute)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") @@ -282,7 +257,7 @@ public async Task Should_create_service_fabric_url_with_query_string_for_statele .Build(); var downstreamRouteHolder = new DownstreamRouteHolder( new List(), - new RouteBuilder().WithDownstreamRoute(downstreamRoute).Build()); + new Route(downstreamRoute)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") @@ -311,7 +286,7 @@ public async Task Should_create_service_fabric_url_with_query_string_for_statefu .Build(); var downstreamRouteHolder = new DownstreamRouteHolder( new List(), - new RouteBuilder().WithDownstreamRoute(downstreamRoute).Build()); + new Route(downstreamRoute)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") @@ -340,7 +315,7 @@ public async Task Should_create_service_fabric_url_with_version_from_upstream_pa .Build(); var routeHolder = new DownstreamRouteHolder( new List(), - new RouteBuilder().WithDownstreamRoute(route).Build()); + new Route(route)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") @@ -377,10 +352,7 @@ public async Task Should_not_remove_additional_query_parameter_when_placeholder_ new("{action}", "1"), new("{servak}", "2"), }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build() - )); + new Route(downstreamRoute))); GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=123456789"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); GivenTheUrlReplacerWillReturn("/Authorized/1?server=2"); @@ -404,9 +376,7 @@ public async Task Should_not_replace_by_empty_scheme() .Build(); var downstreamRouteHolder = new DownstreamRouteHolder( new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build()); + new Route(downstreamRoute)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") @@ -442,9 +412,7 @@ public async Task Should_map_query_parameters_with_different_names() { new("{userId}", "webley"), }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build() + new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); @@ -476,9 +444,7 @@ public async Task Should_map_query_parameters_with_different_names_and_save_old_ { new("{uid}", "webley"), }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build() + new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); @@ -512,9 +478,7 @@ public async Task Should_forward_query_parameters_without_duplicates(string ever { new("{everythingelse}", everythingelse), }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build() + new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000//contracts?{everythingelse}"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); @@ -554,10 +518,7 @@ public async Task Should_fix_issue_748(string upstreamTemplate, string downstrea new(placeholderName, placeholderValue), new("{version}", "v1"), }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() + new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) } )); GivenTheDownstreamRequestUriIs("http://localhost:5000" + requestURL); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); @@ -597,9 +558,7 @@ public async Task Should_map_when_query_parameters_has_same_names_with_placehold new("{roleid}", roleid), new("{everything}", everything), }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(withGetMethod) - .Build() + new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(withGetMethod) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); @@ -634,9 +593,7 @@ public async Task ShouldNotFailToHandleUrlWithSpecialRegexChars(string urlPath) { new("{path}", urlPath), }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(withGetMethod) - .Build() + new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(withGetMethod) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000/{urlPath}"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); @@ -650,9 +607,15 @@ public async Task ShouldNotFailToHandleUrlWithSpecialRegexChars(string urlPath) Assert.Equal((int)HttpStatusCode.OK, _httpContext.Response.StatusCode); } + private static HashSet AsHashSet(IEnumerable collection) => collection.Select(AsHttpMethod).ToHashSet(); + private static HttpMethod AsHttpMethod(string method) => new(method); + private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) { - var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null, null); + var configuration = new InternalConfiguration() + { + ServiceProviderConfiguration = config, + }; _httpContext.Items.SetIInternalConfiguration(configuration); } diff --git a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs index 6977f8269..fd390817b 100644 --- a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs @@ -42,7 +42,7 @@ public async Task NoDownstreamException() { // Arrange _shouldThrowAnException = false; - var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(); _httpContext.Items.Add(nameof(IInternalConfiguration), config); // Act @@ -58,7 +58,7 @@ public async Task DownstreamException() { // Arrange _shouldThrowAnException = true; - var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(); _httpContext.Items.Add(nameof(IInternalConfiguration), config); // Act @@ -73,7 +73,10 @@ public async Task ShouldSetRequestId() { // Arrange _shouldThrowAnException = false; - var config = new InternalConfiguration(null, null, null, "requestidkey", null, null, null, null, null, null); + var config = new InternalConfiguration() + { + RequestId = "requestidkey", + }; _httpContext.Items.Add(nameof(IInternalConfiguration), config); _httpContext.Request.Headers.Append("requestidkey", "1234"); @@ -90,7 +93,7 @@ public async Task ShouldSetAspDotNetRequestId() { // Arrange _shouldThrowAnException = false; - var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(); _httpContext.Items.Add(nameof(IInternalConfiguration), config); _httpContext.Request.Headers.Append("requestidkey", "1234"); diff --git a/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs index ad4bf0855..0253c1265 100644 --- a/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs +++ b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs @@ -17,7 +17,7 @@ public void ShouldNotBuild() // Arrange var configRepo = new Mock(); configRepo.Setup(x => x.Get()) - .Returns(new OkResponse(new InternalConfiguration(null, null, null, null, null, null, null, null, null, null))); + .Returns(new OkResponse(new InternalConfiguration())); var services = new ServiceCollection(); services.AddSingleton(configRepo.Object); var sp = services.BuildServiceProvider(true); @@ -37,7 +37,7 @@ public void ShouldBuild() var client = new Mock(); var configRepo = new Mock(); configRepo.Setup(x => x.Get()) - .Returns(new OkResponse(new InternalConfiguration(null, null, serviceProviderConfig, null, null, null, null, null, null, null))); + .Returns(new OkResponse(new InternalConfiguration() { ServiceProviderConfiguration = serviceProviderConfig })); var services = new ServiceCollection(); services.AddSingleton(configRepo.Object); services.AddSingleton(client.Object); diff --git a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs index 47b7c6f31..d0c257f65 100644 --- a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs @@ -37,18 +37,17 @@ public ClaimsToHeadersMiddlewareTests() public async Task Should_call_add_headers_to_request_correctly() { // Arrange - var downstreamRoute = new DownstreamRouteHolder(new(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("any old string") - .WithClaimsToHeaders(new List - { - new("UserId", "Subject", string.Empty, 0), - }) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + var route = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithClaimsToHeaders(new List + { + new("UserId", "Subject", string.Empty, 0), + }) + .WithUpstreamHttpMethod(["Get"]) + .Build(); + var downstreamRoute = new DownstreamRouteHolder( + new(), + new Route(route, HttpMethod.Get)); // Arrange: Given The Down Stream Route Is _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs index 21d11ced1..8bda2e159 100644 --- a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -82,11 +82,11 @@ private void GivenTheHttpResponseMessageIs() private void GivenTheRouteHasPreFindAndReplaceSetUp() { var fAndRs = new List(); - var route = new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder().WithUpstreamHeaderFindAndReplace(fAndRs) - .WithDownstreamHeaderFindAndReplace(fAndRs).Build()) + var dRoute = new DownstreamRouteBuilder() + .WithUpstreamHeaderFindAndReplace(fAndRs) + .WithDownstreamHeaderFindAndReplace(fAndRs) .Build(); - + var route = new Route(dRoute); var dR = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(null, route); _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(dR.TemplatePlaceholderNameAndValues); diff --git a/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs b/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs index 38d4933ba..157fe33cf 100644 --- a/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs @@ -54,7 +54,7 @@ public void Plural_ThisString(string source, int count, string expected) [InlineData("x", false)] public void IsEmpty(string str, bool expected) { - bool actual = str.IsNullOrEmpty(); + bool actual = str.IsEmpty(); Assert.Equal(expected, actual); } diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs index e1acea0e9..0b309fd8e 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs @@ -1,6 +1,7 @@ using Ocelot.Configuration; using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Creators; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.UnitTests.LoadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs index f09992018..1d28ba543 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.Builder; using Ocelot.Infrastructure; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.UnitTests.Responder; @@ -11,6 +13,7 @@ namespace Ocelot.UnitTests.LoadBalancer; +[Trait("Feat", "336")] public sealed class CookieStickySessionsTests : UnitTest { private readonly CookieStickySessions _stickySessions; @@ -36,22 +39,6 @@ private void Arrange([CallerMemberName] string serviceName = null) _httpContext.Items.UpsertDownstreamRoute(route); } - [Fact] - public async Task Should_expire_sticky_session() - { - Arrange(); - GivenTheLoadBalancerReturns(); - GivenTheDownstreamRequestHasSessionId("321"); - GivenIHackAMessageInWithAPastExpiry(); - - // Act - var result = await _stickySessions.LeaseAsync(_httpContext); - _bus.Process(); - - // Assert - _loadBalancer.Verify(x => x.Release(It.IsAny()), Times.Once); - } - [Fact] public async Task Should_return_host_and_port() { @@ -130,6 +117,22 @@ public async Task Should_return_error() result.IsError.ShouldBeTrue(); } + [Fact] + public async Task Should_expire_sticky_session() + { + Arrange(); + GivenTheLoadBalancerReturns(); + GivenTheDownstreamRequestHasSessionId("321"); + GivenIHackAMessageInWithAPastExpiry(); + + // Act + var result = await _stickySessions.LeaseAsync(_httpContext); + _bus.Process(); + + // Assert + _loadBalancer.Verify(x => x.Release(It.IsAny()), Times.Once); + } + [Fact] public void Should_release() { @@ -137,6 +140,13 @@ public void Should_release() _stickySessions.Release(new ServiceHostAndPort(string.Empty, 0)); } + [Fact] + public void Type_Is_CookieStickySessions() + { + // Arrange, Act, Assert + Assert.Equal("CookieStickySessions", _stickySessions.Type); + } + private void GivenIHackAMessageInWithAPastExpiry() { var hostAndPort = new ServiceHostAndPort("999", 999); diff --git a/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs index da135bb32..c8a0105f1 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Creators; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; @@ -63,6 +64,13 @@ public void Should_return_error() loadBalancer.IsError.ShouldBeTrue(); } + [Fact] + public void Type() + { + // Arrange, Act, Assert + Assert.Equal(nameof(FakeLoadBalancer), _creator.Type); + } + private class FakeLoadBalancer : ILoadBalancer { public FakeLoadBalancer(DownstreamRoute downstreamRoute, IServiceDiscoveryProvider serviceDiscoveryProvider) diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeaseEventArgsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeaseEventArgsTests.cs new file mode 100644 index 000000000..0b73c5819 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LeaseEventArgsTests.cs @@ -0,0 +1,24 @@ +using Ocelot.LoadBalancer; +using Ocelot.Values; + +namespace Ocelot.UnitTests.LoadBalancer; + +public class LeaseEventArgsTests +{ + [Fact] + public void Ctor() + { + // Arrange + ServiceHostAndPort host = new("host", 123); + Lease lease = new(host, 3); + Service service = new("s", new("h", 123), "", "", []); + + // Act + LeaseEventArgs args = new(lease, service, 3); + + // Assert + Assert.Equal(lease, args.Lease); + Assert.Equal(service, args.Service); + Assert.Equal(3, args.ServiceIndex); + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeaseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeaseTests.cs new file mode 100644 index 000000000..8e368b0e5 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LeaseTests.cs @@ -0,0 +1,168 @@ +using Ocelot.LoadBalancer; +using Ocelot.Values; +using Steeltoe.Connector; + +namespace Ocelot.UnitTests.LoadBalancer; + +public class LeaseTests +{ + [Fact] + public void Ctor() + { + // Arrange, Act + Lease l = new(); + + // Assert + Assert.Null(l.HostAndPort); + Assert.Equal(0, l.Connections); + } + + [Fact] + public void Ctor_Lease() + { + // Arrange + ServiceHostAndPort host = new("host", 123); + Lease from = new(host, 3); + + // Act + Lease actual = new(from); + + // Assert + Assert.Equivalent(from, actual); + Assert.Equivalent(3, actual.Connections); + } + + [Fact] + public void Ctor_ServiceHostAndPort() + { + // Arrange + ServiceHostAndPort hostAndPort = new("host", 123); + + // Act + Lease actual = new(hostAndPort); + + // Assert + Assert.Equivalent(hostAndPort, actual.HostAndPort); + Assert.Equal(hostAndPort, actual.HostAndPort); + Assert.Equal(0, actual.Connections); + } + + [Fact] + public void Ctor_Init() + { + // Arrange + ServiceHostAndPort hostAndPort = new("host", 123); + int connections = 3; + + // Act + Lease actual = new(hostAndPort, connections); + + // Assert + Assert.Equivalent(hostAndPort, actual.HostAndPort); + Assert.Equal(hostAndPort, actual.HostAndPort); + Assert.Equal(3, actual.Connections); + } + + [Fact] + public void Null() + { + // Arrange, Act + Lease actual = Lease.Null; + + // Assert + Assert.Null(actual.HostAndPort); + Assert.Equal(0, actual.Connections); + } + + [Fact] + public void ToString_HostPlusConnections() + { + // Arrange + Lease l = new(new("host", 333, "ws"), 4); + + // Act + var actual = l.ToString(); + + // Assert + Assert.NotNull(actual); + Assert.Equal("(ws:host:333+4)", actual); + } + + [Fact] + public void Equals_object() + { + // Arrange, Act, Assert + Lease l = Lease.Null; + var boxed = (object)l; + bool equality = l.Equals(boxed); + Assert.True(equality); + + // Arrange, Act, Assert + l = new(new("host", 333, "ws"), 4); + boxed = (object)l; + equality = Lease.Null.Equals(boxed); + Assert.False(equality); + + // Arrange, Act, Assert + string s = "not Lease"; + boxed = (object)s; + equality = l.Equals(boxed); + Assert.False(equality); + } + + [Fact] + public void Equals_Lease() + { + // Arrange, Act, Assert : false + Lease l = new(new("host", 333, "ws"), 4); + Lease other = Lease.Null; + bool equality = l.Equals(other); + Assert.False(equality); + + // Arrange, Act, Assert : true + equality = Lease.Null.Equals(other); + Assert.True(equality); + } + + [Fact] + public void Op_Inequality_Lease_Lease() + { + // Arrange, Act, Assert : true + Lease x = new(new("host", 333, "ws"), 4); + Lease y = Lease.Null; + bool equality = x != y; + Assert.True(equality); + + // Arrange, Act, Assert : false + equality = Lease.Null != y; + Assert.False(equality); + } + + [Fact] + public void Op_Inequality_ServiceHostAndPort_Lease() + { + // Arrange, Act, Assert : false + ServiceHostAndPort h = new("host", 333, "ws"); + Lease l = new(h, 1); + bool equality = h != l; + Assert.False(equality); + + // Arrange, Act, Assert : true + equality = h != Lease.Null; + Assert.True(equality); + } + + [Fact] + public void Op_Inequality_Lease_ServiceHostAndPort() + { + // Arrange, Act, Assert : false + ServiceHostAndPort h = new("host", 333, "ws"); + Lease l = new(h, 1); + bool equality = l != h; + Assert.False(equality); + + // Arrange, Act, Assert : true + equality = Lease.Null != h; + Assert.True(equality); + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs index e546301be..79662fb12 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs @@ -1,6 +1,8 @@ using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.ServiceDiscovery.Providers; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Creators; +using Ocelot.ServiceDiscovery.Providers; +using System.Reflection; namespace Ocelot.UnitTests.LoadBalancer; @@ -15,19 +17,26 @@ public LeastConnectionCreatorTests() _serviceProvider = new(); } - [Fact] - public void Should_return_instance_of_expected_load_balancer_type() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Should_return_instance_of_expected_load_balancer_type(bool isNullServiceName) { // Arrange var route = new DownstreamRouteBuilder() - .WithServiceName("myService") + .WithServiceName(isNullServiceName ? null : "myService") + .WithLoadBalancerKey("key") .Build(); // Act - var loadBalancer = _creator.Create(route, _serviceProvider.Object); + var response = _creator.Create(route, _serviceProvider.Object); // Assert - loadBalancer.Data.ShouldBeOfType(); + response.Data.ShouldBeOfType(); + var balancer = response.Data as LeastConnection; + var field = balancer.GetType().GetField("_serviceName", BindingFlags.Instance | BindingFlags.NonPublic); + var serviceName = field.GetValue(balancer) as string; + serviceName.ShouldBe(isNullServiceName ? "key" : "myService"); } [Fact] diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs index b2f5bc384..2f3a3c046 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs @@ -1,6 +1,9 @@ using Microsoft.AspNetCore.Http; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Balancers; using Ocelot.Values; +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.LoadBalancer; namespace Ocelot.UnitTests.LoadBalancer; @@ -221,7 +224,6 @@ public async Task Should_return_error_if_services_are_empty() { // Arrange const string ServiceName = "products"; - var hostAndPort = new ServiceHostAndPort("localhost", 80); _leastConnection = new LeastConnection(() => Task.FromResult(new List()), ServiceName); // Act @@ -231,4 +233,32 @@ public async Task Should_return_error_if_services_are_empty() result.IsError.ShouldBeTrue(); result.Errors[0].ShouldBeOfType(); } + + [Fact] + public async Task OnLeased() + { + // Arrange + const string ServiceName = "products"; + var availableServices = new List + { + new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), + }; + var leastConnection = new TestLeastConnection(() => Task.FromResult(availableServices), ServiceName); + + // Act + var result = await leastConnection.LeaseAsync(_httpContext); + + // Assert + leastConnection.Events.ShouldNotBeEmpty(); + var args = leastConnection.Events[0].ShouldNotBeNull(); + args.Service.Name.ShouldBe(ServiceName); + } +} + +internal sealed class TestLeastConnection : LeastConnection, ILoadBalancer +{ + public readonly List Events = new(); + public TestLeastConnection(Func>> services, string serviceName) + : base(services, serviceName) => Leased += Me_Leased; + private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args); } diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs index dbd046216..31749dcf2 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs @@ -2,7 +2,10 @@ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Infrastructure.RequestData; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; @@ -36,7 +39,7 @@ public void Should_return_no_load_balancer_by_default() { // Arrange var route = new DownstreamRouteBuilder() - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) + .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); @@ -54,7 +57,7 @@ public void Should_return_matching_load_balancer() // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancerTwo), string.Empty, 0)) - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) + .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); @@ -72,7 +75,7 @@ public void Should_return_error_response_if_cannot_find_load_balancer_creator() // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions("DoesntExistLoadBalancer", string.Empty, 0)) - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) + .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); @@ -91,7 +94,7 @@ public void Should_return_error_response_if_creator_errors() // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(BrokenLoadBalancer), string.Empty, 0)) - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) + .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); @@ -109,7 +112,7 @@ public void Should_call_service_provider() // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancerOne), string.Empty, 0)) - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) + .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); @@ -127,7 +130,7 @@ public void Should_return_error_response_when_call_to_service_provider_fails() // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancerOne), string.Empty, 0)) - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) + .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryFails(); @@ -172,7 +175,8 @@ private class BrokenLoadBalancerCreator : ILoadBalancerCreator where T : ILoadBalancer, new() { public BrokenLoadBalancerCreator() => Type = typeof(T).Name; - public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) => new ErrorResponse(new ErrorInvokingLoadBalancerCreator(new Exception())); + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + => new ErrorResponse(new InvokingLoadBalancerCreatorError(new Exception())); public string Type { get; } } diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs index fcb397948..66164f2d3 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -1,7 +1,10 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; @@ -124,6 +127,58 @@ public void Should_get_new_load_balancer_if_route_load_balancer_has_changed() result.Data.ShouldBeOfType(); } + [Fact] + public void GetResponse_IsError() + { + // Arrange + var route = new DownstreamRouteBuilder() + .WithLoadBalancerKey("test") + .Build(); + var loadBalancer = new FakeLoadBalancer(); + _factory.Setup(x => x.Get(route, _serviceProviderConfig)) + .Returns(new ErrorResponse(new CouldNotFindLoadBalancerCreatorError($"Could not find load balancer creator for Type: FakeLoadBalancer, please check your config specified the correct load balancer and that you have registered a class with the same name."))); + + // Act + var result = _house.Get(route, _serviceProviderConfig); + + // Assert + result.IsError.ShouldBeTrue(); + result.ShouldBeOfType>(); + result.Data.ShouldBeNull(); + _factory.Verify(x => x.Get(route, _serviceProviderConfig), Times.Once); + result.Errors.Single().ShouldBeOfType(); + } + + [Fact] + public void TypesMismatched_ShouldReturnError() + { + // Arrange + var route = new DownstreamRouteBuilder() + .WithLoadBalancerKey("test") + .Build(); + var loadBalancer = new FakeLoadBalancer(); + _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse(loadBalancer)); + + // Other route has the same LoadBalancerKey but types are different + var route2 = new DownstreamRouteBuilder() + .WithLoadBalancerKey(route.LoadBalancerKey) + .WithLoadBalancerOptions(new() { Type = "bla-bla" }) + .Build(); + _factory.Setup(x => x.Get(route2, _serviceProviderConfig)) + .Returns(new ErrorResponse(new CouldNotFindLoadBalancerCreatorError($"Could not find load balancer creator for Type: {route2.LoadBalancerOptions.Type}, please check your config specified the correct load balancer and that you have registered a class with the same name."))); + + // Act + var result = _house.Get(route2, _serviceProviderConfig); + + // Assert: Then It Is Added + result.IsError.ShouldBeTrue(); + result.ShouldBeOfType>(); + result.Data.ShouldBeNull(); + _factory.Verify(x => x.Get(route2, _serviceProviderConfig), Times.Once); + result.Errors.Single().ShouldBeOfType() + .Message.ShouldBe("Could not find load balancer creator for Type: bla-bla, please check your config specified the correct load balancer and that you have registered a class with the same name."); + } + private class FakeLoadBalancer : ILoadBalancer { public string Type => nameof(FakeLoadBalancer); diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs index a9bc8b376..1d7066db5 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -3,8 +3,9 @@ using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Errors; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.LoadBalancer.Middleware; +using Ocelot.LoadBalancer; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; @@ -24,7 +25,7 @@ public class LoadBalancerMiddlewareTests : UnitTest private readonly Mock _loggerFactory; private readonly Mock _logger; private LoadBalancingMiddleware _middleware; - private readonly RequestDelegate _next; + private RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public LoadBalancerMiddlewareTests() @@ -42,18 +43,22 @@ public LoadBalancerMiddlewareTests() .Returns(new OkResponse(_loadBalancer.Object)); } - [Fact] - public async Task Should_call_scoped_data_repository_correctly() + private void Arrange() { - // Arrange var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) + .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var serviceProviderConfig = new ServiceProviderConfigurationBuilder() .Build(); GivenTheDownStreamUrlIs("http://my.url/abc?q=123"); GivenTheConfigurationIs(serviceProviderConfig); GivenTheDownStreamRouteIs(downstreamRoute, new List()); + } + + [Fact] + public async Task Should_call_scoped_data_repository_correctly() + { + Arrange(); // Arrange: Given The Load Balancer Returns _hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); @@ -71,15 +76,7 @@ public async Task Should_call_scoped_data_repository_correctly() [Fact] public async Task Should_set_pipeline_error_if_cannot_get_load_balancer() { - // Arrange - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) - .Build(); - var serviceProviderConfig = new ServiceProviderConfigurationBuilder() - .Build(); - GivenTheDownStreamUrlIs("http://my.url/abc?q=123"); - GivenTheConfigurationIs(serviceProviderConfig); - GivenTheDownStreamRouteIs(downstreamRoute, new List()); + Arrange(); // Arrange: Given The Load Balancer House Returns An Error _getLoadBalancerHouseError = new ErrorResponse(new List @@ -101,15 +98,7 @@ public async Task Should_set_pipeline_error_if_cannot_get_load_balancer() [Fact] public async Task Should_set_pipeline_error_if_cannot_get_least() { - // Arrange - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) - .Build(); - var serviceProviderConfig = new ServiceProviderConfigurationBuilder() - .Build(); - GivenTheDownStreamUrlIs("http://my.url/abc?q=123"); - GivenTheConfigurationIs(serviceProviderConfig); - GivenTheDownStreamRouteIs(downstreamRoute, new List()); + Arrange(); // Arrange: Given The Load Balancer Returns An Error _getHostAndPortError = new ErrorResponse(new List { new ServicesAreNullError("services were null for bah") }); @@ -128,15 +117,7 @@ public async Task Should_set_pipeline_error_if_cannot_get_least() [Fact] public async Task Should_set_scheme() { - // Arrange - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamHttpMethod(new() { HttpMethods.Get }) - .Build(); - var serviceProviderConfig = new ServiceProviderConfigurationBuilder() - .Build(); - GivenTheDownStreamUrlIs("http://my.url/abc?q=123"); - GivenTheConfigurationIs(serviceProviderConfig); - GivenTheDownStreamRouteIs(downstreamRoute, new List()); + Arrange(); // Arrange: Given The Load Balancer Returns Ok _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny())) @@ -152,9 +133,31 @@ public async Task Should_set_scheme() _httpContext.Items.DownstreamRequest().Scheme.ShouldBeEquivalentTo("https"); } + [Fact] + public async Task ShouldNot_LogDebug_WhenNextMiddlewareThrownException() + { + Arrange(); + _next = (context) => throw new NotImplementedException("NextMiddleware"); + + // Arrange: Given The Load Balancer Returns Ok + _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny())) + .ReturnsAsync(new OkResponse(new ServiceHostAndPort("abc", 123, "https"))); + _logger.Setup(x => x.LogDebug(It.IsAny())).Verifiable(); + + // Act + _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object); + var action = () => _middleware.Invoke(_httpContext); + var ex = await action.ShouldThrowAsync(); + ex.Message.ShouldBe("NextMiddleware"); + _logger.Verify(x => x.LogDebug(It.IsAny()), Times.Never); + } + private void GivenTheConfigurationIs(ServiceProviderConfiguration config) { - var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null, null); + var configuration = new InternalConfiguration() + { + ServiceProviderConfiguration = config, + }; _httpContext.Items.SetIInternalConfiguration(configuration); } diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsCreatorTests.cs new file mode 100644 index 000000000..e4c13f7a6 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsCreatorTests.cs @@ -0,0 +1,287 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.Balancers; +using Shouldly; +using System.Reflection; + +namespace Ocelot.UnitTests.LoadBalancer; + +public class LoadBalancerOptionsCreatorTests : UnitTest +{ + private readonly LoadBalancerOptionsCreator _creator = new(); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Should_create(bool isNull) + { + // Arrange + var options = isNull ? null : + new FileLoadBalancerOptions + { + Type = "test", + Key = "west", + Expiry = 1, + }; + + // Act + var result = _creator.Create(options); + + // Assert + result.Type.ShouldBe(isNull ? "NoLoadBalancer" : "test"); + result.Key.ShouldBe(isNull ? null : "west"); + result.ExpiryInMs.ShouldBe(isNull ? 0 : 1); + } + + [Fact] + [Trait("PR", "2324")] + [Trait("Feat", "2319")] + public void Create_FromRoute_NullChecks() + { + // Arrange, Act, Assert + FileRoute route = null; + FileGlobalConfiguration globalConfiguration = null; + var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); + Assert.Equal(nameof(route), actual.ParamName); + + // Arrange, Act, Assert 2 + route = new(); + globalConfiguration = null; + actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); + Assert.Equal(nameof(globalConfiguration), actual.ParamName); + } + + [Fact] + [Trait("PR", "2324")] + [Trait("Feat", "2319")] + public void Create_FromRoute() + { + // Arrange + FileRoute route = new() + { + LoadBalancerOptions = new() + { + Key = "route", + }, + }; + FileGlobalConfiguration globalConfiguration = new() + { + LoadBalancerOptions = new("global"), + }; + + // Act + var actual = _creator.Create(route, globalConfiguration); + + // Assert + Assert.Equal("global", actual.Type); + Assert.Equal("route", actual.Key); + Assert.Equal(0, actual.ExpiryInMs); + } + + [Fact] + [Trait("PR", "2324")] + [Trait("Feat", "2319")] + public void Create_FromDynamicRoute_NullChecks() + { + // Arrange, Act, Assert + FileDynamicRoute route = null; + FileGlobalConfiguration globalConfiguration = null; + var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); + Assert.Equal(nameof(route), actual.ParamName); + + // Arrange, Act, Assert 2 + route = new(); + globalConfiguration = null; + actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); + Assert.Equal(nameof(globalConfiguration), actual.ParamName); + } + + [Fact] + [Trait("PR", "2324")] + [Trait("Feat", "2319")] + public void Create_FromDynamicRoute() + { + // Arrange + FileDynamicRoute route = new() + { + LoadBalancerOptions = new() + { + Key = "route", + }, + }; + FileGlobalConfiguration globalConfiguration = new() + { + LoadBalancerOptions = new("global"), + }; + + // Act + var actual = _creator.Create(route, globalConfiguration); + + // Assert + Assert.Equal("global", actual.Type); + Assert.Equal("route", actual.Key); + Assert.Equal(0, actual.ExpiryInMs); + } + + [Fact] + [Trait("PR", "2324")] + [Trait("Feat", "2319")] + public void CreateProtected_NullCheck() + { + // Arrange + var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); + IRouteGrouping grouping = null; + FileLoadBalancerOptions options = null; + FileGlobalLoadBalancerOptions globalOptions = null; + + // Act + var wrapper = Assert.Throws( + () => method.Invoke(_creator, [grouping, options, globalOptions])); + + // Assert + Assert.IsType(wrapper.InnerException); + var actual = (ArgumentNullException)wrapper.InnerException; + Assert.Equal(nameof(grouping), actual.ParamName); + } + + [Fact] + [Trait("PR", "2324")] + [Trait("Feat", "2319")] + public void CreateProtected() + { + // Arrange + var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); + FileDynamicRoute route = new() { Key = "r1" }; + FileLoadBalancerOptions options = null; + FileGlobalLoadBalancerOptions globalOptions = new() + { + RouteKeys = null, + Type = "global", + Key = "global", + Expiry = 1, + }; + + // Act, Assert + var actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); + Assert.Equal("global", actual.Type); + Assert.Equal("global", actual.Key); + Assert.Equal(1, actual.ExpiryInMs); + + // Arrange 2 + options = new() + { + Type = "route", + Key = "route", + Expiry = 3, + }; + globalOptions.RouteKeys = ["?"]; + + // Act, Assert 2 + actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); + Assert.Equal("route", actual.Type); + Assert.Equal("route", actual.Key); + Assert.Equal(3, actual.ExpiryInMs); + + globalOptions.RouteKeys = ["r1"]; + actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); + Assert.Equal("route", actual.Type); + Assert.Equal("route", actual.Key); + Assert.Equal(3, actual.ExpiryInMs); + + globalOptions = null; + actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); + Assert.Equal("route", actual.Type); + Assert.Equal("route", actual.Key); + Assert.Equal(3, actual.ExpiryInMs); + + // Arrange 3 + options.Key = null; + globalOptions = new() + { + RouteKeys = null, + Type = "global", + Key = "global", + Expiry = 1, + }; + actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); + Assert.Equal("route", actual.Type); + Assert.Equal("global", actual.Key); + Assert.Equal(3, actual.ExpiryInMs); + } + + [Fact] + [Trait("PR", "2324")] + [Trait("Feat", "2319")] + public void CreateProtected_NoOptions() + { + // Arrange + var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); + FileDynamicRoute route = new(); + FileLoadBalancerOptions options = null; + FileGlobalLoadBalancerOptions globalOptions = null; + + // Act + var actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); + + // Assert + Assert.Equal(nameof(NoLoadBalancer), actual.Type); + } + + [Fact] + [Trait("PR", "2324")] + [Trait("Feat", "2319")] + + public void Merge_NullCheck() + { + // Arrange + var method = _creator.GetType().GetMethod("Merge", BindingFlags.Instance | BindingFlags.NonPublic); + FileLoadBalancerOptions options = null; + FileLoadBalancerOptions globalOptions = null; + + // Act, Assert 1 + var wrapper = Assert.Throws( + () => method.Invoke(_creator, [null, globalOptions])); + Assert.IsType(wrapper.InnerException); + var actual = (ArgumentNullException)wrapper.InnerException; + Assert.Equal(nameof(options), actual.ParamName); + + // Act, Assert 2 + options = new(); + wrapper = Assert.Throws( + () => method.Invoke(_creator, [options, null])); + Assert.IsType(wrapper.InnerException); + actual = (ArgumentNullException)wrapper.InnerException; + Assert.Equal(nameof(globalOptions), actual.ParamName); + } + + [Theory] + [Trait("PR", "2324")] + [Trait("Feat", "2319")] + [InlineData(false)] + [InlineData(true)] + public void Merge(bool isDef) + { + // Arrange + var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic); + FileLoadBalancerOptions options = new() + { + Type = isDef ? string.Empty : "route", + Key = isDef ? string.Empty : "route", + Expiry = isDef ? null : 1, + }; + FileLoadBalancerOptions globalOptions = new("global") + { + Key = "global", + Expiry = 3, + }; + + // Act + var actual = (LoadBalancerOptions)method.Invoke(_creator, [options, globalOptions]); + + // Assert + Assert.Equal(isDef ? "global" : "route", actual.Type); + Assert.Equal(isDef ? "global" : "route", actual.Key); + Assert.Equal(isDef ? 3 : 1, actual.ExpiryInMs); + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs index ba131cbac..4cb869d1c 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs @@ -1,17 +1,102 @@ using Ocelot.Configuration; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.Balancers; namespace Ocelot.UnitTests.LoadBalancer; public class LoadBalancerOptionsTests { [Fact] - public void Should_default_to_no_load_balancer() + public void Ctor_ShouldDefaultToNoLoadBalancer() { // Arrange, Act - var options = new LoadBalancerOptionsBuilder().Build(); + LoadBalancerOptions options = new(); + LoadBalancerOptions options2 = new(default, default, default); + LoadBalancerOptions options3 = new(new FileLoadBalancerOptions()); // Assert - options.Type.ShouldBe(nameof(NoLoadBalancer)); + Assert.Equal(nameof(NoLoadBalancer), options.Type); + Assert.Equal(nameof(NoLoadBalancer), options2.Type); + Assert.Equal(nameof(NoLoadBalancer), options3.Type); + } + + [Fact] + public void Ctor_Parameterless() + { + // Arrange, Act + LoadBalancerOptions actual = new(); + + // Assert + Assert.Equal(nameof(NoLoadBalancer), actual.Type); + Assert.Null(actual.Key); + Assert.Equal(0, actual.ExpiryInMs); + } + + [Fact] + [Trait("PR", "2324")] + public void Ctor_CopyingFrom_FileLoadBalancerOptions() + { + // Arrange + FileLoadBalancerOptions from = new() + { + Type = "Balancer", + Key = "BalancerKey", + Expiry = 3, + }; + + // Act + LoadBalancerOptions actual = new(from); + + // Assert + Assert.False(ReferenceEquals(from, actual)); + Assert.Equal("Balancer", actual.Type); + Assert.Equal("BalancerKey", actual.Key); + Assert.Equal(3, actual.ExpiryInMs); + } + + [Theory] + [Trait("PR", "2324")] + [InlineData(false)] + [InlineData(true)] + public void Ctor_Initialization3Params(bool isDef) + { + // Arrange + FileLoadBalancerOptions from = new() + { + Type = isDef ? string.Empty : "TestBalancer", + Key = isDef ? string.Empty : "Bla-Bla", + Expiry = isDef ? null : 3, + }; + + // Act + LoadBalancerOptions actual = new(from.Type, from.Key, from.Expiry); + + // Assert + Assert.Equal(isDef ? nameof(NoLoadBalancer) : "TestBalancer", actual.Type); + Assert.Equal(isDef ? string.Empty : "Bla-Bla", actual.Key); + Assert.Equal(isDef ? 0 : 3, actual.ExpiryInMs); + } + + [Theory] + [Trait("PR", "2324")] + [InlineData(false)] + [InlineData(true)] + public void Ctor_Initialization_CookieStickySessions(bool isDef) + { + // Arrange + FileLoadBalancerOptions from = new() + { + Type = nameof(CookieStickySessions), + Key = isDef ? string.Empty : "Key", + Expiry = isDef ? null : 3, + }; + + // Act + LoadBalancerOptions actual = new(from.Type, from.Key, from.Expiry); + + // Assert + Assert.Equal("CookieStickySessions", actual.Type); + Assert.Equal(isDef ? ".AspNetCore.Session" : "Key", actual.Key); + Assert.Equal(isDef ? 1200000 : 3, actual.ExpiryInMs); } } diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs index bd0a6c10d..29d134cee 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs @@ -1,5 +1,6 @@ using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Creators; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.UnitTests.LoadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs index 278f3f105..5dd23c1c5 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Http; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Balancers; using Ocelot.Responses; using Ocelot.Values; @@ -78,4 +78,20 @@ public async Task Should_return_error_if_null_services() // Assert _result.IsError.ShouldBeTrue(); } + + [Fact] + public async Task Release() + { + // Arrange + var hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); + var services = new List + { + new("product", hostAndPort, string.Empty, string.Empty, Array.Empty()), + }; + _services.AddRange(services); + _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext()); + + // Act + _loadBalancer.Release(hostAndPort); + } } diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs index f9bbad03f..8a7b05ba8 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs @@ -1,7 +1,8 @@ using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Responses; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Creators; using Ocelot.ServiceDiscovery.Providers; +using System.Reflection; namespace Ocelot.UnitTests.LoadBalancer; @@ -16,17 +17,26 @@ public RoundRobinCreatorTests() _serviceProvider = new Mock(); } - [Fact] - public void Should_return_instance_of_expected_load_balancer_type() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Should_return_instance_of_expected_load_balancer_type(bool isNullServiceName) { // Arrange - var route = new DownstreamRouteBuilder().Build(); + var route = new DownstreamRouteBuilder() + .WithServiceName(isNullServiceName ? null : "myService") + .WithLoadBalancerKey("key") + .Build(); // Act var loadBalancer = _creator.Create(route, _serviceProvider.Object); // Assert loadBalancer.Data.ShouldBeOfType(); + var balancer = loadBalancer.Data as RoundRobin; + var field = balancer.GetType().GetField("_serviceName", BindingFlags.Instance | BindingFlags.NonPublic); + var serviceName = field.GetValue(balancer) as string; + serviceName.ShouldBe(isNullServiceName ? "key" : "myService"); } [Fact] diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs index 09c543ec7..345238605 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs @@ -1,8 +1,12 @@ using Microsoft.AspNetCore.Http; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer; +using Ocelot.LoadBalancer.Balancers; +using Ocelot.LoadBalancer.Errors; +using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; using System.Diagnostics; +using System.Reflection; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.LoadBalancer; @@ -103,6 +107,114 @@ public void Lease_LoopThroughIndexRangeIndefinitelyUnderHighLoad_ShouldDistribut counters.ShouldAllBe(counter => bottom <= counter && counter <= top, message); } + [Fact] + public async Task OnLeased() + { + // Arrange + const string ServiceName = "products"; + var availableServices = new List + { + new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), + }; + var roundRobin = new TestRoundRobin(() => Task.FromResult(availableServices), ServiceName); + + // Act + var result = await roundRobin.LeaseAsync(_httpContext); + + // Assert + Assert.NotEmpty(roundRobin.Events); + var args = roundRobin.Events[0]; + Assert.NotNull(args); + Assert.Equal(ServiceName, args.Service.Name); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task LeaseAsync_ServicesAreEmpty_ServicesAreEmptyError(bool isNull) + { + // Arrange + List services = isNull ? null : GivenServices(0); + var roundRobin = GivenLoadBalancer(services); + + // Act + var actual = await roundRobin.LeaseAsync(_httpContext); + + // Assert + Assert.True(actual.IsError); + var error = actual.Errors[0]; + Assert.IsType(error); + Assert.Equal("There were no services in RoundRobin for 'LeaseAsync_ServicesAreEmpty_ServicesAreEmptyError' during LeaseAsync operation!", error.Message); + } + + [Fact] + public async Task Release() + { + // Arrange + const string ServiceName = nameof(Release); + var availableServices = new List + { + new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), + }; + var roundRobin = new RoundRobin(() => Task.FromResult(availableServices), ServiceName); + var response = await roundRobin.LeaseAsync(_httpContext); + + // Act, Assert + roundRobin.Release(response.Data); + } + + [Fact] + public void TryScanNext() + { + // Arrange + const int lastIndex = 3; + var method = typeof(RoundRobin).GetMethod(nameof(TryScanNext), BindingFlags.Instance | BindingFlags.NonPublic); + var field = typeof(RoundRobin).GetField("LastIndices", BindingFlags.Static | BindingFlags.NonPublic); + List services = GivenServices(lastIndex); + var roundRobin = GivenLoadBalancer(services); + var lastIndices = field.GetValue(roundRobin) as Dictionary; + lastIndices[nameof(TryScanNext)] = lastIndex; + + // Act + // TryScanNext(Service[] readme, out Service next, out int index) + var readme = services.ToArray(); + Service next = null; + int index = -1; + object[] parameters = [readme, next, index]; + bool success = (bool)method.Invoke(roundRobin, parameters); + + // Assert + Assert.True(success); + Assert.Equal(0, parameters[2]); + Assert.Equal(readme[0], parameters[1]); + Assert.Equal(1, lastIndices[nameof(TryScanNext)]); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Update_CanIncreaseConnections(bool increase) + { + var method = typeof(RoundRobin).GetMethod("Update", BindingFlags.Instance | BindingFlags.NonPublic); + var field = typeof(RoundRobin).GetField("_leasing", BindingFlags.Instance | BindingFlags.NonPublic); + List services = GivenServices(1); + var roundRobin = GivenLoadBalancer(services); + Lease item = new( + services[0].HostAndPort, + increase ? 0 : 1); + var leasing = field.GetValue(roundRobin) as List; + leasing.Add(item); + + // Act + // int Update(ref Lease item, bool increase) + object[] parameters = [item, increase]; + int index = (int)method.Invoke(roundRobin, parameters); + + Lease actual = (Lease)parameters[0]; + Assert.Equal(0, index); + Assert.Equal(increase ? 1 : 0, actual.Connections); + } + private static int[] CountServices(List services, Response[] responses) { var counters = new int[services.Count]; @@ -157,3 +269,11 @@ private static RoundRobin GivenLoadBalancer(List services, bool immedia serviceName); } } + +internal sealed class TestRoundRobin : RoundRobin, ILoadBalancer +{ + public readonly List Events = new(); + public TestRoundRobin(Func>> services, string serviceName) + : base(services, serviceName) => Leased += Me_Leased; + private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args); +} diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs index a78b8e3b7..6f84300cc 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs @@ -5,7 +5,7 @@ using Ocelot.DependencyInjection; using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.DownstreamUrlCreator.Middleware; -using Ocelot.LoadBalancer.Middleware; +using Ocelot.LoadBalancer; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.WebSockets; diff --git a/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs index d929b1079..3c97915ff 100644 --- a/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration.Builder; +using Ocelot.Configuration; using Ocelot.Multiplexer; using static Ocelot.UnitTests.Multiplexing.UserDefinedResponseAggregatorTests; @@ -13,9 +13,10 @@ public class DefinedAggregatorProviderTests : UnitTest public void Should_find_aggregator() { // Arrange - var route = new RouteBuilder() - .WithAggregator("TestDefinedAggregator") - .Build(); + var route = new Route() + { + Aggregator = "TestDefinedAggregator", + }; var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(true); @@ -34,9 +35,10 @@ public void Should_find_aggregator() public void Should_not_find_aggregator() { // Arrange - var route = new RouteBuilder() - .WithAggregator("TestDefinedAggregator") - .Build(); + var route = new Route() + { + Aggregator = "TestDefinedAggregator", + }; // Arrange: Given No Defined Aggregator var serviceCollection = new ServiceCollection(); diff --git a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs index c398da6ab..7817f646b 100644 --- a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs @@ -52,7 +52,7 @@ public async Task Should_multiplex() [Fact] public async Task Should_not_multiplex() { - var route = new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().Build()).Build(); + var route = new Route(new DownstreamRouteBuilder().Build()); GivenTheFollowing(route); // Act @@ -325,13 +325,13 @@ private void AssertUsers(HttpContext actual) private static Route GivenDefaultRoute(int count) { - var b = new RouteBuilder(); + var r = new Route(); for (var i = 0; i < count; i++) { - b.WithDownstreamRoute(new DownstreamRouteBuilder().Build()); + r.DownstreamRoute.Add(new DownstreamRouteBuilder().Build()); } - - return b.Build(); + + return r; } private static Route GivenRoutesWithAggregator() @@ -340,20 +340,15 @@ private static Route GivenRoutesWithAggregator() var route2 = new DownstreamRouteBuilder().WithKey("UserDetails").Build(); var route3 = new DownstreamRouteBuilder().WithKey("PostDetails").Build(); - var b = new RouteBuilder(); - b.WithDownstreamRoute(route1); - b.WithDownstreamRoute(route2); - b.WithDownstreamRoute(route3); - - b.WithAggregateRouteConfig(new() - { - new AggregateRouteConfig { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, - new AggregateRouteConfig { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, - }); - - b.WithAggregator("TestAggregator"); - - return b.Build(); + return new Route() + { + DownstreamRoute = [route1, route2, route3], + DownstreamRouteConfig = [ + new AggregateRouteConfig { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, + new AggregateRouteConfig { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, + ], + Aggregator = "TestAggregator", + }; } private void GivenTheFollowing(Route route) diff --git a/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs index 0b6104c66..059e28774 100644 --- a/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs @@ -1,5 +1,4 @@ using Ocelot.Configuration; -using Ocelot.Configuration.Builder; using Ocelot.Multiplexer; namespace Ocelot.UnitTests.Multiplexing; @@ -21,7 +20,7 @@ public ResponseAggregatorFactoryTests() public void Should_return_simple_json_aggregator() { // Arrange - var route = new RouteBuilder().Build(); + var route = new Route(); // Act _aggregator = _factory.Get(route); @@ -34,9 +33,10 @@ public void Should_return_simple_json_aggregator() public void Should_return_user_defined_aggregator() { // Arrange - var route = new RouteBuilder() - .WithAggregator("doesntmatter") - .Build(); + var route = new Route() + { + Aggregator = "doesntmatter", + }; // Act _aggregator = _factory.Get(route); diff --git a/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs index dec8996b6..c0e15678d 100644 --- a/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; -using Ocelot.Configuration.File; using Ocelot.Middleware; using Ocelot.Multiplexer; using Ocelot.UnitTests.Responder; @@ -24,24 +23,21 @@ public SimpleJsonResponseAggregatorTests() public async Task Should_aggregate_n_responses_and_set_response_content_on_upstream_context_withConfig() { var commentsDownstreamRoute = new DownstreamRouteBuilder().WithKey("Comments").Build(); - var userDetailsDownstreamRoute = new DownstreamRouteBuilder().WithKey("UserDetails") .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 0, false, "/v1/users/{userId}")) .Build(); - var downstreamRoutes = new List { commentsDownstreamRoute, userDetailsDownstreamRoute, }; - - var route = new RouteBuilder() - .WithDownstreamRoutes(downstreamRoutes) - .WithAggregateRouteConfig(new List - { + var route = new Route() + { + DownstreamRoute = downstreamRoutes, + DownstreamRouteConfig = [ new(){RouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"}, - }) - .Build(); + ], + }; var commentsResponseContent = @"[{string.Emptyidstring.Empty:1,string.EmptywriterIdstring.Empty:1,string.EmptypostIdstring.Empty:1,string.Emptytextstring.Empty:string.Emptytext1string.Empty},{string.Emptyidstring.Empty:2,string.EmptywriterIdstring.Empty:2,string.EmptypostIdstring.Empty:2,string.Emptytextstring.Empty:string.Emptytext2string.Empty},{string.Emptyidstring.Empty:3,string.EmptywriterIdstring.Empty:2,string.EmptypostIdstring.Empty:1,string.Emptytextstring.Empty:string.Emptytext21string.Empty}]"; @@ -71,18 +67,16 @@ public async Task Should_aggregate_n_responses_and_set_response_content_on_upstr public async Task Should_aggregate_n_responses_and_set_response_content_on_upstream_context() { var billDownstreamRoute = new DownstreamRouteBuilder().WithKey("Bill").Build(); - var georgeDownstreamRoute = new DownstreamRouteBuilder().WithKey("George").Build(); - var downstreamRoutes = new List { billDownstreamRoute, georgeDownstreamRoute, }; - - var route = new RouteBuilder() - .WithDownstreamRoutes(downstreamRoutes) - .Build(); + var route = new Route() + { + DownstreamRoute = downstreamRoutes, + }; var billDownstreamContext = new DefaultHttpContext(); billDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new EditableList>>(), "some reason")); @@ -109,18 +103,16 @@ public async Task Should_aggregate_n_responses_and_set_response_content_on_upstr public async Task Should_return_error_if_any_downstreams_have_errored() { var billDownstreamRoute = new DownstreamRouteBuilder().WithKey("Bill").Build(); - var georgeDownstreamRoute = new DownstreamRouteBuilder().WithKey("George").Build(); - var downstreamRoutes = new List { billDownstreamRoute, georgeDownstreamRoute, }; - - var route = new RouteBuilder() - .WithDownstreamRoutes(downstreamRoutes) - .Build(); + var route = new Route() + { + DownstreamRoute = downstreamRoutes, + }; var billDownstreamContext = new DefaultHttpContext(); billDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new List>>(), "some reason")); diff --git a/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs index b9e8d38ee..762689045 100644 --- a/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs @@ -23,7 +23,7 @@ public UserDefinedResponseAggregatorTests() public async Task Should_call_aggregator() { // Arrange - var route = new RouteBuilder().Build(); + var route = new Route(); var context = new DefaultHttpContext(); var contextA = new DefaultHttpContext(); @@ -55,7 +55,7 @@ public async Task Should_call_aggregator() public async Task Should_not_find_aggregator() { // Arrange - var route = new RouteBuilder().Build(); + var route = new Route(); var context = new DefaultHttpContext(); var contextA = new DefaultHttpContext(); diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index ca9f25a86..458a31a36 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -43,7 +43,7 @@ - + diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs index 7b1857f6c..3213a8630 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs @@ -668,6 +668,7 @@ private static DownstreamRoute GivenDownstreamRoute(string routeTemplate, int? e return new DownstreamRouteBuilder() .WithQosOptions(options) .WithUpstreamPathTemplate(upstreamPath) + .WithLoadBalancerKey($"{routeTemplate}|no-host|localhost:20005,localhost:20007|no-svc-ns|no-svc-name|LeastConnection|no-lb-key") .Build(); } } diff --git a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs index 41d7a4145..e9f758973 100644 --- a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs @@ -37,19 +37,17 @@ public ClaimsToQueryStringMiddlewareTests() public async Task Should_call_add_queries_correctly() { // Arrange + var route = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithClaimsToQueries(new List + { + new("UserId", "Subject", string.Empty, 0), + }) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); var downstreamRoute = new DownstreamRouteHolder( new(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("any old string") - .WithClaimsToQueries(new List - { - new("UserId", "Subject", string.Empty, 0), - }) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + new Route(route, HttpMethod.Get)); _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); _addQueries.Setup(x => x.SetQueriesOnDownstreamRequest(It.IsAny>(), It.IsAny>(), It.IsAny())) diff --git a/test/Ocelot.UnitTests/RateLimiting/FileGlobalRateLimitingTests.cs b/test/Ocelot.UnitTests/RateLimiting/FileGlobalRateLimitingTests.cs index 4bfe40c5e..51560c663 100644 --- a/test/Ocelot.UnitTests/RateLimiting/FileGlobalRateLimitingTests.cs +++ b/test/Ocelot.UnitTests/RateLimiting/FileGlobalRateLimitingTests.cs @@ -28,11 +28,27 @@ public void FileGlobalRateLimitByAspNetRule_Ctor() [Fact] public void FileGlobalRateLimitByHeaderRule_Ctor() { - // Arrange, Act + // Arrange, Act, Assert FileGlobalRateLimitByHeaderRule actual = new(); + Assert.Null(actual.RouteKeys); + + // Arrange + FileRateLimitByHeaderRule from = new() + { + ClientIdHeader = "1", + ClientWhitelist = ["2"], + DisableRateLimitHeaders = true, + HttpStatusCode = 4, + QuotaExceededMessage = "5", + RateLimitCounterPrefix = "6", + }; + + // Act + FileGlobalRateLimitByHeaderRule actualG = new(from); // Assert - Assert.Null(actual.RouteKeys); + Assert.False(ReferenceEquals(from, actualG)); + Assert.Equivalent(from, actualG); } [Fact] diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitOptionsCreatorTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitOptionsCreatorTests.cs index d45533211..2fbddace1 100644 --- a/test/Ocelot.UnitTests/RateLimiting/RateLimitOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitOptionsCreatorTests.cs @@ -246,4 +246,27 @@ public void MergeHeaderRules_FromGlobal() Assert.Equal("12/9s/w10s", actual.Rule.ToString()); } #endregion + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Create_FileGlobalConfiguration(bool hasOptions) + { + // Arrange + FileGlobalConfiguration global = new() + { + RateLimitOptions = !hasOptions ? null : new() + { + ClientIdHeader = "globalRule", + }, + }; + + // Act + var actual = _creator.Create(global); + + // Assert + Assert.NotNull(actual); + Assert.Equal(hasOptions ? "globalRule" : "Oc-Client", actual.ClientIdHeader); + Assert.Equal(hasOptions, actual.EnableRateLimiting); + } } diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs index 638698429..4eba538ce 100644 --- a/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs @@ -275,10 +275,11 @@ private static DownstreamRoute GivenDownstreamRoute(RateLimitOptions options = n .WithLoadBalancerKey(testName) .Build(); - private static Route GivenRoute(DownstreamRoute dr) => new RouteBuilder() - .WithDownstreamRoute(dr) - .WithUpstreamHttpMethod([HttpMethods.Get]) - .Build(); + private static Route GivenRoute(DownstreamRoute dr) => new() + { + DownstreamRoute = [dr], + UpstreamHttpMethod = [HttpMethod.Get], + }; private async Task> WhenICallTheMiddlewareMultipleTimes(long times, _DownstreamRouteHolder_ holder, string header = null, HttpContext originalContext = null) { diff --git a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs index 71a6e96af..c34281ef8 100644 --- a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; @@ -41,15 +42,14 @@ public RequestIdMiddlewareTests() public async Task Should_pass_down_request_id_from_upstream_request() { // Arrange - var downstreamRoute = new DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() + var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + .WithUpstreamHttpMethod(["Get"]) + .Build(); + var downstreamRoute = new DownstreamRouteHolder( + new List(), + new Route(route, HttpMethod.Get)); var requestId = Guid.NewGuid().ToString(); @@ -68,15 +68,14 @@ public async Task Should_pass_down_request_id_from_upstream_request() public async Task Should_add_request_id_when_not_on_upstream_request() { // Arrange - var downstreamRoute = new DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() + var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + .WithUpstreamHttpMethod(["Get"]) + .Build(); + var downstreamRoute = new DownstreamRouteHolder( + new List(), + new Route(route, HttpMethod.Get)); GivenTheDownStreamRouteIs(downstreamRoute); GivenThereIsNoGlobalRequestId(); @@ -93,15 +92,14 @@ public async Task Should_add_request_id_when_not_on_upstream_request() public async Task Should_add_request_id_scoped_repo_for_logging_later() { // Arrange - var downstreamRoute = new DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() + var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + .Build(); + var downstreamRoute = new DownstreamRouteHolder( + new List(), + new Route(route, HttpMethod.Get)); var requestId = Guid.NewGuid().ToString(); @@ -121,15 +119,14 @@ public async Task Should_add_request_id_scoped_repo_for_logging_later() public async Task Should_update_request_id_scoped_repo_for_logging_later() { // Arrange - var downstreamRoute = new DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() + var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + .WithUpstreamHttpMethod(["Get"]) + .Build(); + var downstreamRoute = new DownstreamRouteHolder( + new List(), + new Route(route, HttpMethod.Get)); var requestId = Guid.NewGuid().ToString(); @@ -149,15 +146,14 @@ public async Task Should_update_request_id_scoped_repo_for_logging_later() public async Task Should_not_update_if_global_request_id_is_same_as_re_route_request_id() { // Arrange - var downstreamRoute = new DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() + var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); + .Build(); + var downstreamRoute = new DownstreamRouteHolder( + new List(), + new Route(route, HttpMethod.Get)); var requestId = "alreadyset"; diff --git a/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs b/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs index 992439b98..3efa332fd 100644 --- a/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs +++ b/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs @@ -375,7 +375,7 @@ private static Mock GetHandlerFactory() .WithLoadBalancerKey(string.Empty) .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, false, 10, TimeSpan.FromSeconds(120))) - .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamHttpMethod(["Get"]) .Build(); }