diff --git a/.circleci/config.yml b/.circleci/config.yml index cbdcf7c9d..114d2b2e1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,29 +1,30 @@ version: 2.1 orbs: - queue: eddiewebb/queue@2.2.1 + queue: eddiewebb/queue@3.1.4 jobs: build: docker: - image: ocelot2/circleci-build:latest + resource_class: medium+ steps: - checkout - - run: dotnet tool restore && dotnet cake + - run: dotnet dev-certs https && dotnet tool restore && dotnet cake release: docker: - image: ocelot2/circleci-build:latest steps: - checkout - - run: dotnet tool restore && dotnet cake --target=Release + - run: dotnet dev-certs https && dotnet tool restore && dotnet cake --target=Release workflows: version: 2 main: jobs: - - queue/block_workflow: - time: '20' - only-on-branch: main + # - queue/block_workflow: + # time: '20' + # only-on-branch: main - release: - requires: - - queue/block_workflow + # requires: + # - queue/block_workflow filters: branches: only: main @@ -33,7 +34,7 @@ workflows: filters: branches: only: develop - pr: + PR: jobs: - build: filters: diff --git a/.editorconfig b/.editorconfig index e4e769b52..e8766a5e0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,15 +1,246 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories root = true -[*] -end_of_line = lf -insert_final_newline = true +# XML files +[*.xml] +indent_style = space +indent_size = 2 +# C# files [*.cs] -end_of_line = lf -indent_style = space -indent_size = 4 -# XML files -[*.xml] +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 indent_style = space -indent_size = 2 +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = false:suggestion +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = false:suggestion +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +insert_final_newline = true +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion diff --git a/.gitignore b/.gitignore index 1c30928ce..f01a1095e 100644 --- a/.gitignore +++ b/.gitignore @@ -417,5 +417,4 @@ test/Ocelot.AcceptanceTests/ocelot.json # Read the Docs # https://ocelot.readthedocs.io _build/ -_static/ _templates/ diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 4fe1ec98f..000000000 --- a/Directory.Build.props +++ /dev/null @@ -1,15 +0,0 @@ - - - latest - git - https://github.com/ThreeMammals/Ocelot - - true - - true - snupkg - - - - - diff --git a/Ocelot.Release.sln b/Ocelot.Release.sln new file mode 100644 index 000000000..20a76ee83 --- /dev/null +++ b/Ocelot.Release.sln @@ -0,0 +1,231 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" + ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore + .editorconfig = .editorconfig + .gitignore = .gitignore + .readthedocs.yaml = .readthedocs.yaml + build.cake = build.cake + build.ps1 = build.ps1 + codeanalysis.ruleset = codeanalysis.ruleset + .circleci\config.yml = .circleci\config.yml + GitVersion.yml = GitVersion.yml + LICENSE.md = LICENSE.md + README.md = README.md + ReleaseNotes.md = ReleaseNotes.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.csproj", "{54E84F1A-E525-4443-96EC-039CBD50C263}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.csproj", "{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.ManualTest", "test\Ocelot.ManualTest\Ocelot.ManualTest.csproj", "{02BBF4C5-517E-4157-8D21-4B8B9E118B7A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.csproj", "{106B49E6-95F6-4A7B-B81C-96BFA74AF035}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Administration", "src\Ocelot.Administration\Ocelot.Administration.csproj", "{F69CEF43-27D2-4940-A47A-FCA879E371BC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Cache.CacheManager", "src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj", "{EB9F438F-062E-499F-B6EA-4412BEF6D74C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Consul", "src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj", "{02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Eureka", "src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj", "{9BBD3586-145C-4FA0-91C5-9ED58287D753}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Kubernetes", "src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{72C8E528-B4F5-45CE-8A06-CD3787364856}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.OpenTracing", "src\Ocelot.Tracing.OpenTracing\Ocelot.Tracing.OpenTracing.csproj", "{11C622AD-8C0A-4CF4-811B-3DBB76550797}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "samples", "{8FA0CBA0-0338-48EB-B37F-83CA5022237C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.AdministrationApi", "samples\Administration\Ocelot.Samples.AdministrationApi.csproj", "{A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Basic.ApiGateway", "samples\Basic\Ocelot.Samples.Basic.ApiGateway.csproj", "{F00C73F4-019D-490D-8194-CA1754D717FA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.ApiGateway", "samples\Eureka\ApiGateway\Ocelot.Samples.Eureka.ApiGateway.csproj", "{FECB0C8B-5778-4441-B10E-0C815F5106D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.DownstreamService", "samples\Eureka\DownstreamService\Ocelot.Samples.Eureka.DownstreamService.csproj", "{28AD7065-8DB1-4711-83BF-9EA47D75F8F7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.GraphQL", "samples\GraphQL\Ocelot.Samples.GraphQL.csproj", "{869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.ApiGateway", "samples\Kubernetes\ApiGateway\Ocelot.Samples.Kubernetes.ApiGateway.csproj", "{681B6E08-114D-4B9B-8F82-E370CA29B8EC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.DownstreamService", "samples\Kubernetes\DownstreamService\Ocelot.Samples.Kubernetes.DownstreamService.csproj", "{161DD558-993D-491B-AD20-966127D71E49}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OpenTracing", "samples\OpenTracing\Ocelot.Samples.OpenTracing.csproj", "{DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "samples\ServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{F25EA682-A763-431B-9D88-012A388D3618}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\ServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{DCBD0AB5-85DD-4F28-9166-0A23969E19EC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.ApiGateway", "samples\ServiceFabric\ApiGateway\Ocelot.Samples.ServiceFabric.ApiGateway.csproj", "{D991C694-01F0-4F04-8135-5C133DC8E029}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.DownstreamService", "samples\ServiceFabric\DownstreamService\Ocelot.Samples.ServiceFabric.DownstreamService.csproj", "{AD09D124-7DD7-4C9E-9BCC-782B579B1786}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.Build.0 = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.Build.0 = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.Build.0 = Release|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.Build.0 = Debug|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.ActiveCfg = Release|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.Build.0 = Release|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.Build.0 = Release|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Release|Any CPU.Build.0 = Release|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Release|Any CPU.Build.0 = Release|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.Build.0 = Release|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.Build.0 = Release|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.Build.0 = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Release|Any CPU.Build.0 = Release|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Release|Any CPU.Build.0 = Release|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Release|Any CPU.Build.0 = Release|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Release|Any CPU.Build.0 = Release|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Release|Any CPU.Build.0 = Release|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Release|Any CPU.Build.0 = Release|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Release|Any CPU.Build.0 = Release|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Release|Any CPU.Build.0 = Release|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Release|Any CPU.Build.0 = Release|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Release|Any CPU.Build.0 = Release|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Release|Any CPU.Build.0 = Release|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {54E84F1A-E525-4443-96EC-039CBD50C263} = {5B401523-36DA-4491-B73A-7590A26E420B} + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {5B401523-36DA-4491-B73A-7590A26E420B} + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B} + {106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B} + {D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B} + {F69CEF43-27D2-4940-A47A-FCA879E371BC} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {EB9F438F-062E-499F-B6EA-4412BEF6D74C} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {9BBD3586-145C-4FA0-91C5-9ED58287D753} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {F00C73F4-019D-490D-8194-CA1754D717FA} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {FECB0C8B-5778-4441-B10E-0C815F5106D5} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {681B6E08-114D-4B9B-8F82-E370CA29B8EC} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {161DD558-993D-491B-AD20-966127D71E49} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {F25EA682-A763-431B-9D88-012A388D3618} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {D991C694-01F0-4F04-8135-5C133DC8E029} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {AD09D124-7DD7-4C9E-9BCC-782B579B1786} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} + EndGlobalSection +EndGlobal diff --git a/Ocelot.sln b/Ocelot.sln index e40f83cfb..f09456c44 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.8.34309.116 +VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" EndProject @@ -23,8 +22,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.csproj", "{54E84F1A-E525-4443-96EC-039CBD50C263}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.csproj", "{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}" @@ -35,6 +32,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Benchmarks", "test\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Administration", "src\Ocelot.Administration\Ocelot.Administration.csproj", "{F69CEF43-27D2-4940-A47A-FCA879E371BC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Cache.CacheManager", "src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj", "{EB9F438F-062E-499F-B6EA-4412BEF6D74C}" @@ -43,68 +44,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Consul", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Eureka", "src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj", "{9BBD3586-145C-4FA0-91C5-9ED58287D753}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Kubernetes", "src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{72C8E528-B4F5-45CE-8A06-CD3787364856}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8FA0CBA0-0338-48EB-B37F-83CA5022237C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotBasic.ApiGateway", "samples\OcelotBasic\Ocelot.Samples.OcelotBasic.ApiGateway.csproj", "{ED0B3A09-112B-4BA4-82D6-11569BC7A99B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdministrationApi", "samples\AdministrationApi\AdministrationApi.csproj", "{B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotGraphQL", "samples\OcelotGraphQL\OcelotGraphQL.csproj", "{F43429C3-EC49-464F-9423-9118A36E8FE3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eureka", "eureka", "{F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiGateway", "samples\OcelotEureka\ApiGateway\ApiGateway.csproj", "{48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DownstreamService", "samples\OcelotEureka\DownstreamService\DownstreamService.csproj", "{32ADF9B3-CBFA-4607-8A8E-1532D90A7197}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "k8s", "k8s", "{4B706988-4817-43A8-ABE1-32A67998C2C8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotKube.ApiGateway", "samples\OcelotKube\ApiGateway\Ocelot.Samples.OcelotKube.ApiGateway.csproj", "{8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotKube.DownstreamService", "samples\OcelotKube\DownstreamService\Ocelot.Samples.OcelotKube.DownstreamService.csproj", "{7B319B8C-8155-4779-BD93-5ABD05CA2AB6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "service-fabric", "service-fabric", "{B412628F-C325-47E1-A8D9-873DE04C8AF5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotApplicationApiGateway", "samples\OcelotServiceFabric\src\OcelotApplicationApiGateway\OcelotApplicationApiGateway.csproj", "{8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotApplicationService", "samples\OcelotServiceFabric\src\OcelotApplicationService\OcelotApplicationService.csproj", "{33BE6D88-F188-4E60-83AC-3C4B94D24675}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "administration", "administration", "{1F1F324D-6EA4-4E63-A6A7-C6053F412F1A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "basic", "basic", "{ED066001-BAF7-4117-9884-DF591A56347D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "graphql", "graphql", "{C15CD120-5F8D-41DE-9B21-00E3EA77D6C1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.OpenTracing", "src\Ocelot.Tracing.OpenTracing\Ocelot.Tracing.OpenTracing.csproj", "{11C622AD-8C0A-4CF4-811B-3DBB76550797}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "open-tracing", "open-tracing", "{731C6A8A-69ED-445C-A132-C638AA93F9C7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotOpenTracing", "samples\OcelotOpenTracing\OcelotOpenTracing.csproj", "{C9427E78-4281-4F59-A66E-17C0B66550E5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "service-discovery", "service-discovery", "{25C30AAA-12DD-4BA5-A53F-9271E54EBAB7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "samples\OcelotServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{D37209EA-C13E-42AE-B851-A8604F1FCD0E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\OcelotServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{E2AC741A-4120-4D59-B5E4-16382ED45E8D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.Build.0 = Debug|Any CPU {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -125,6 +78,14 @@ Global {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.Build.0 = Debug|Any CPU {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -141,6 +102,10 @@ Global {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.Build.0 = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -149,106 +114,30 @@ Global {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.Build.0 = Debug|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.ActiveCfg = Release|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.Build.0 = Release|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Release|Any CPU.Build.0 = Release|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Release|Any CPU.Build.0 = Release|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Release|Any CPU.Build.0 = Release|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Release|Any CPU.Build.0 = Release|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Debug|Any CPU.Build.0 = Debug|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Release|Any CPU.Build.0 = Release|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Release|Any CPU.Build.0 = Release|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Release|Any CPU.Build.0 = Release|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Release|Any CPU.Build.0 = Release|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Debug|Any CPU.Build.0 = Debug|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.ActiveCfg = Release|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.Build.0 = Release|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.Build.0 = Debug|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.ActiveCfg = Release|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.Build.0 = Release|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.Build.0 = Release|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.Build.0 = Release|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.Build.0 = Release|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {54E84F1A-E525-4443-96EC-039CBD50C263} = {5B401523-36DA-4491-B73A-7590A26E420B} {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {5B401523-36DA-4491-B73A-7590A26E420B} {02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B} {106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B} {D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B} + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {F69CEF43-27D2-4940-A47A-FCA879E371BC} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {EB9F438F-062E-499F-B6EA-4412BEF6D74C} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {9BBD3586-145C-4FA0-91C5-9ED58287D753} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B} = {ED066001-BAF7-4117-9884-DF591A56347D} - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E} = {1F1F324D-6EA4-4E63-A6A7-C6053F412F1A} - {F43429C3-EC49-464F-9423-9118A36E8FE3} = {C15CD120-5F8D-41DE-9B21-00E3EA77D6C1} - {F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29} = {F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF} - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197} = {F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF} - {4B706988-4817-43A8-ABE1-32A67998C2C8} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16} = {4B706988-4817-43A8-ABE1-32A67998C2C8} - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6} = {4B706988-4817-43A8-ABE1-32A67998C2C8} - {B412628F-C325-47E1-A8D9-873DE04C8AF5} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590} = {B412628F-C325-47E1-A8D9-873DE04C8AF5} - {33BE6D88-F188-4E60-83AC-3C4B94D24675} = {B412628F-C325-47E1-A8D9-873DE04C8AF5} - {1F1F324D-6EA4-4E63-A6A7-C6053F412F1A} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {ED066001-BAF7-4117-9884-DF591A56347D} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {C15CD120-5F8D-41DE-9B21-00E3EA77D6C1} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {731C6A8A-69ED-445C-A132-C638AA93F9C7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {C9427E78-4281-4F59-A66E-17C0B66550E5} = {731C6A8A-69ED-445C-A132-C638AA93F9C7} - {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {D37209EA-C13E-42AE-B851-A8604F1FCD0E} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} - {E2AC741A-4120-4D59-B5E4-16382ED45E8D} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 48db3c3bf..3a0da1bdb 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,31 +1,84 @@ -## Hotfix release (version {0}) for #2031 issue -> Route path template placeholders and their validation rules +## Spring 2024 (version {0}) aka [Twilight Texas](https://www.timeanddate.com/eclipse/solar/2024-april-8) release +> Codenamed: **[Twilight Texas](https://www.timeanddate.com/eclipse/solar/2024-april-8)** +> Read the Docs: [Ocelot 23.3](https://ocelot.readthedocs.io/en/{0}/) -Special thanks to **[Guillaume Gnaegi](https://github.com/ggnaegi)** and [Fabrizio Mancin](https://github.com/Fabman08)! +### What's new? -### About -The bug is related to the [Placeholders](https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders) feature in [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) and [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html). -The bug was introduced in version [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0) as a part of PR #1927. +- **[Service Discovery](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/servicediscovery.rst)**: Introducing a new feature for "[Customization of services creation](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/servicediscovery.rst#consul-service-builder-3)" in two primary service discovery providers: [Consul](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/servicediscovery.rst#consul-service-builder-3) and [Kubernetes](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/kubernetes.rst#downstream-scheme-vs-port-names-3), developed by @raman-m. + The customization for both `Consul` and `Kube` providers in service creation is achieved through the overriding of virtual methods in default implementations. The recommendation was to separate the provider's logic and introduce `public virtual` and `protected virtual` methods in concrete classes, enabling: + - The use of `public virtual` methods as dictated by interface definitions. + - The application of `protected virtual` methods to allow developers to customize atomic operations through inheritance from existing concrete classes. + - The injection of new interface objects into the provider's constructor. + - The overriding of the default behavior of classes. -### Breaking Change -The new [validation rules](https://github.com/ThreeMammals/Ocelot/blob/23.2.0/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs#L45-L50) of the `FileConfigurationFluentValidator` class do not allow the Ocelot app to start when implicit [placeholders](https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders) are defined in custom implementations, such as middlewares, delegating handlers, and replaced services in the dependency injection (DI) container. -These new rules are capable of validating explicit [placeholders](https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders) only within the `UpstreamPathTemplate` and `DownstreamPathTemplate` properties. Unfortunately, they cannot oversee implicit placeholders in custom implementations, and they do not validate early during the Ocelot app startup process. + Ultimately, customization relies on the virtual methods within the default implementation classes, providing developers the flexibility to override them as necessary for highly tailored Consul/K8s configurations in their specific environments. + For further details, refer to the respective pull requests for both providers: + - `Kube` → PR #2052 + - `Consul` → PR #2067 -Ensure that you avoid using version [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0). If you are currently on that version, upgrade to version [{0}](https://github.com/ThreeMammals/Ocelot/releases/tag/{0}) by applying this hotfix patch. +- **[Routing](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/routing.rst)**: Introducing the new "[Routing based on Request Header](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/routing.rst#upstream-headers-3)" feature by @jlukawska. + In addition to routing via `UpstreamPathTemplate`, you can now define an `UpstreamHeaderTemplates` options dictionary. For a route to match, all headers specified in this section are required to be present in the request headers. + For more details, see PR #1312. -### Technical info -With version [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0), particularly if you have overridden certain service classes or implemented custom logic that manipulates placeholders, you may encounter Ocelot app crashes accompanied by the following errors in the log: -``` -One or more errors occurred. (Unable to start Ocelot, errors are: XXX) -``` -where `XXX` are the following validation error messages: -- `UpstreamPathTemplate 'UUU' doesn't contain the same placeholders in DownstreamPathTemplate 'DDD'` -- `DownstreamPathTemplate 'DDD' doesn't contain the same placeholders in UpstreamPathTemplate 'UUU'` +- **[Configuration](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/configuration.rst)**: Introducing the "[Custom Default Version Policy](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/configuration.rst#downstreamhttpversionpolicy-3)" feature by @ibnuda. + The configurable `HttpRequestMessage.VersionPolicy` helps avoid HTTP protocol connection errors and stabilizes connections to downstream services, especially when you're not developing those services, documentation is scarce, or the deployed HTTP protocol version is uncertain. + For developers of downstream services, it's possible to `ConfigureKestrel` server and its endpoints with new protocol settings. However, attention to version policy is also required, and this feature provides precise version settings for HTTP connections. + Essentially, this feature promotes the use of HTTP protocols beyond 1.0/1.1, such as HTTP/2 or even HTTP/3. + For additional details, refer to PR #1673. -**Finally**, the [validation rules](https://github.com/ThreeMammals/Ocelot/blob/23.2.0/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs#L45-L50) resulted from the incorrect assumption that placeholders are always explicit and can be validated early. Therefore, custom implementations and feature services in the dependency injection (DI) container, which rely on or manipulate placeholders, should validate the configuration JSON and appropriate options later, directly within their service implementations. +- **[Configuration](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/configuration.rst)**: Introducing the new "[Route Metadata](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/configuration.rst#route-metadata)" feature by @vantm. Undoubtedly, this is the standout feature of the release! :star: + Route metadata enables Ocelot developers to incorporate custom functions that address specific needs or to create their own plugins/extensions. + In versions of Ocelot prior to [{0}](https://github.com/ThreeMammals/Ocelot/releases/tag/{0}), the configuration was limited to predefined values that Ocelot used internally. This was sufficient for official extensions, but posed challenges for third-party developers who needed to implement configurations not included in the standard `FileConfiguration`. Applying an option to a specific route required knowledge of the array index and other details that might not be readily accessible using the standard `IConfiguration` or `IOptions` models from ASP.NET. Now, metadata can be directly accessed in the `DownstreamRoute` object. Furthermore, metadata can also be retrieved from the global JSON section via the `FileConfiguration.GlobalConfiguration` property. + For more information, see the details in PR #1843 on this remarkable feature. -### Bug Artifacts -- Released in version: [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0) -- Introduced in: PR #1927 -- Reported bug: #2031 by @ggnaegi and tested by @Fabman08 -- Hotfix PR: #2032 by @raman-m +### Focus On + +
+ Updates of the features: Configuration, Service Discovery, Routing and Quality of Service + + - [Configuration](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/configuration.rst): New features are "[Custom Default Version Policy](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/configuration.rst#downstreamhttpversionpolicy-3)" by @ibnuda and "[Route Metadata](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/configuration.rst#route-metadata)" by @vantm. + + - [Service Discovery](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/servicediscovery.rst): New feature is "[Customization of services creation](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/servicediscovery.rst#consul-service-builder-3)" by @raman-m. + + - [Routing](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/routing.rst): New feature is "[Routing based on Request Header](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/routing.rst#upstream-headers-3)" by @jlukawska. + + - [Quality of Service](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/qualityofservice.rst): The team has decided to remove the Polly V7 policies logic and the corresponding Ocelot `AddPollyV7` extensions (referenced in PR #2079). Furthermore, the Polly V8 Circuit Breaker has been mandated as the primary strategy (as per PR #2086). + See more detaild below in "**Ocelot extra packages**" paragraph. +
+ +
+ Ocelot extra packages + + - **[Ocelot.Provider.Polly](https://www.nuget.org/packages/Ocelot.Provider.Polly)** + + - Our team has resolved to eliminate the Polly V7 policies logic and the corresponding Ocelot `AddPollyV7` extensions entirely (refer to the "[Polly v7 vs v8](https://github.com/ThreeMammals/Ocelot/blob/23.2.0/docs/features/qualityofservice.rst#polly-v7-vs-v8)" documentation). In the previous [23.2](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0) release, named [Lunar Eclipse](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0), we included these to maintain the legacy Polly behavior, allowing development teams to transition or retain the old Polly V7 functionality. We are now confident that it is time to progress alongside Polly, shifting our focus to the new Polly V8 [resilience pipelines](https://www.pollydocs.org/pipelines/). For more details, see PR #2079. + + - Additionally, we have implemented Polly v8 Circuit Breaker as the primary strategy. Our Quality of Service (QoS) relies on two main strategies: [Circuit Breaker](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/qualityofservice.rst#circuit-breaker-strategy) and [Timeout](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/qualityofservice.rst#timeout-strategy). If both Circuit Breaker and Timeout are [configured](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/qualityofservice.rst#configuration) with their respective properties in the `QoSOptions` of the route JSON, then the Circuit Breaker strategy will take precedence in the constructed resilience pipeline. For more details, refer to PR #2086. +
+ +
+ Stabilization aka bug fixing + + - Fixed #2034 in PR #2045 by @raman-m + - Fixed #2039 in PR #2050 by @PaulARoy + - Fixed #1590 in PR #1592 by @sergio-str + - Fixed #2054 #2059 in PR #2058 by @thiagoloureiro + - Fixed #954 #957 #1026 in PR #2067 by @raman-m + - Fixed #2002 in PR #2003 by @bbenameur + - Fixed #2085 in PR #2086 by @RaynaldM + - See [all bugs](https://github.com/ThreeMammals/Ocelot/issues?q=is%3Aissue+milestone%3ASpring%2724+is%3Aclosed+label%3Abug) of the [Spring'24](https://github.com/ThreeMammals/Ocelot/milestone/6) milestone +
+ +
+ Documentation for version {0} + + - [Caching](https://ocelot.readthedocs.io/en/{0}/features/caching.html): New [EnableContentHashing option](https://ocelot.readthedocs.io/en/{0}/features/caching.html#enablecontenthashing-option) and [Global Configuration](https://ocelot.readthedocs.io/en/{0}/features/caching.html#global-configuration) sections + - [Configuration](https://ocelot.readthedocs.io/en/{0}/features/configuration.html): New [DownstreamHttpVersionPolicy](https://ocelot.readthedocs.io/en/{0}/features/configuration.html#downstreamhttpversionpolicy-3) and [Route Metadata](https://ocelot.readthedocs.io/en/{0}/features/configuration.html#route-metadata) + - [Kubernetes](https://ocelot.readthedocs.io/en/{0}/features/kubernetes.html): New [Downstream Scheme vs Port Names](https://ocelot.readthedocs.io/en/{0}/features/kubernetes.html#downstream-scheme-vs-port-names-3) section + - [Metadata](https://ocelot.readthedocs.io/en/{0}/features/metadata.html): This is new chapter for [Route Metadata](https://ocelot.readthedocs.io/en/{0}/features/configuration.html#route-metadata) feature. + - [Quality of Service](https://ocelot.readthedocs.io/en/{0}/features/qualityofservice.html) + - [Rate Limiting](https://ocelot.readthedocs.io/en/{0}/features/ratelimiting.html) + - [Request Aggregation](https://ocelot.readthedocs.io/en/{0}/features/requestaggregation.html) + - [Routing](https://ocelot.readthedocs.io/en/{0}/features/routing.html): New [Upstream Headers](https://ocelot.readthedocs.io/en/{0}/features/routing.html#upstream-headers-3) section + - [Service Discovery](https://ocelot.readthedocs.io/en/{0}/features/servicediscovery.html): New [Consul Service Builder](https://ocelot.readthedocs.io/en/{0}/features/servicediscovery.html#consul-service-builder-3) section +
diff --git a/build.cake b/build.cake index f7896932f..430a1c804 100644 --- a/build.cake +++ b/build.cake @@ -1,9 +1,9 @@ -#tool "dotnet:?package=GitVersion.Tool&version=5.8.1" -#tool "dotnet:?package=coveralls.net&version=4.0.1" -#addin nuget:?package=Newtonsoft.Json -#addin nuget:?package=System.Text.Encodings.Web&version=4.7.1 -#tool "nuget:?package=ReportGenerator&version=5.2.0" -#addin Cake.Coveralls&version=1.1.0 +#tool dotnet:?package=GitVersion.Tool&version=5.12.0 // 6.0.0-beta.7 supports .NET 8, 7, 6 +#tool dotnet:?package=coveralls.net&version=4.0.1 +#tool nuget:?package=ReportGenerator&version=5.2.4 +#addin nuget:?package=Newtonsoft.Json&version=13.0.3 +#addin nuget:?package=System.Text.Encodings.Web&version=8.0.0 +#addin nuget:?package=Cake.Coveralls&version=1.1.0 #r "Spectre.Console" using Spectre.Console @@ -13,10 +13,8 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -// compile -var compileConfig = Argument("configuration", "Release"); - -var slnFile = "./Ocelot.sln"; +const string Release = "Release"; // task name, target, and Release config name +var compileConfig = Argument("configuration", Release); // compile // build artifacts var artifactsDir = Directory("artifacts"); @@ -61,9 +59,10 @@ string gitHubUsername = "TomPallister"; string gitHubPassword = Environment.GetEnvironmentVariable("OCELOT_GITHUB_API_KEY"); var target = Argument("target", "Default"); - -Information("target is " + target); -Information("Build configuration is " + compileConfig); +var slnFile = (target == Release) ? $"./Ocelot.{Release}.sln" : "./Ocelot.sln"; +Information("\nTarget: " + target); +Information("Build: " + compileConfig); +Information("Solution: " + slnFile); TaskTeardown(context => { AnsiConsole.Markup($"[green]DONE[/] {context.Task.Name}\n"); @@ -83,7 +82,7 @@ Task("RunTests") .IsDependentOn("RunAcceptanceTests") .IsDependentOn("RunIntegrationTests"); -Task("Release") +Task(Release) .IsDependentOn("Build") .IsDependentOn("CreateReleaseNotes") .IsDependentOn("CreateArtifacts") @@ -95,11 +94,18 @@ Task("Compile") .IsDependentOn("Version") .Does(() => { + Information("Build: " + compileConfig); + Information("Solution: " + slnFile); var settings = new DotNetBuildSettings { Configuration = compileConfig, }; - + if (target != Release) + { + settings.Framework = "net8.0"; // build using .NET 8 SDK only + } + Information($"Settings {nameof(DotNetBuildSettings.Framework)}: {settings.Framework}"); + Information($"Settings {nameof(DotNetBuildSettings.Configuration)}: {settings.Configuration}"); DotNetBuild(slnFile, settings); }); @@ -168,9 +174,10 @@ Task("CreateReleaseNotes") return; } - var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary {lastRelease}..HEAD") + var debugUserEmail = false; + var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary --email {lastRelease}..HEAD") .ToList(); - var re = new Regex(@"^[\s\t]*(?'commits'\d+)[\s\t]+(?'author'.*)$"); + var re = new Regex(@"^[\s\t]*(?'commits'\d+)[\s\t]+(?'author'.*)[\s\t]+<(?'email'.*)>.*$"); var summary = shortlogSummary .Where(x => re.IsMatch(x)) .Select(x => re.Match(x)) @@ -178,6 +185,7 @@ Task("CreateReleaseNotes") { commits = int.Parse(m.Groups["commits"]?.Value ?? "0"), author = m.Groups["author"]?.Value?.Trim() ?? string.Empty, + email = m.Groups["email"]?.Value?.Trim() ?? string.Empty, }) .ToList(); @@ -186,13 +194,18 @@ Task("CreateReleaseNotes") foreach (var contributor in summary) { var stars = string.Join(string.Empty, Enumerable.Repeat(":star:", contributor.commits)); - starring.Add($"{stars} {contributor.author}"); + var emailInfo = debugUserEmail ? ", " + contributor.email : string.Empty; + starring.Add($"{stars} {contributor.author}{emailInfo}"); } // Honoring aka Top Contributors const int top3 = 3; // going to create Top 3 var topContributors = new List(); + // Ocelot Core team members should not be in Top 3 Chart + var coreTeamNames = new List { "Raman Maksimchuk", "Raynald Messié", "Guillaume Gnaegi" }; + var coreTeamEmails = new List { "dotnet044@gmail.com", "redbird_project@yahoo.fr", "58469901+ggnaegi@users.noreply.github.com" }; var commitsGrouping = summary + .Where(x => !coreTeamNames.Contains(x.author) && !coreTeamEmails.Contains(x.email)) // filter out Ocelot Core team members .GroupBy(x => x.commits) .Select(g => new { @@ -204,7 +217,7 @@ Task("CreateReleaseNotes") .ToList(); // local helpers - string[] places = new[] { "1st", "2nd", "3rd" }; + string[] places = new[] { "1st", "2nd", "3rd", "4", "5", "6", "7", "8", "9", "10", "11" }; static string Plural(int n) => n == 1 ? "" : "s"; static string Honor(string place, string author, int commits, string suffix = null) => $"{place[0]}{place[1..]} :{place}_place_medal: goes to **{author}** for delivering **{commits}** feature{Plural(commits)} {suffix ?? ""}"; @@ -306,11 +319,11 @@ Task("CreateReleaseNotes") } } // END of Top 3 - // releaseNotes.Add("### Honoring :medal_sports: aka Top Contributors :clap:"); - // releaseNotes.AddRange(topContributors); - // releaseNotes.Add(""); - // releaseNotes.Add("### Starring :star: aka Release Influencers :bowtie:"); - // releaseNotes.AddRange(starring); + releaseNotes.Add("### Honoring :medal_sports: aka Top Contributors :clap:"); + releaseNotes.AddRange(topContributors); + releaseNotes.Add(""); + releaseNotes.Add("### Starring :star: aka Release Influencers :bowtie:"); + releaseNotes.AddRange(starring); releaseNotes.Add(""); releaseNotes.Add($"### Features in Release {releaseVersion}"); var commitsHistory = GitHelper($"log --no-merges --date=format:\"%A, %B %d at %H:%M\" --pretty=format:\"%h by **%aN** on %ad →%n%s\" {lastRelease}..HEAD"); @@ -344,15 +357,23 @@ Task("RunUnitTests") { Configuration = compileConfig, ResultsDirectory = artifactsForUnitTestsDir, - ArgumentCustomization = args => args - // this create the code coverage report - .Append("--collect:\"XPlat Code Coverage\"") + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") + .Append("--collect:\"XPlat Code Coverage\"") // this create the code coverage report + .Append("--verbosity:detailed") + .Append("--consoleLoggerParameters:ErrorsOnly") }; - + if (target != Release) + { + testSettings.Framework = "net8.0"; // .NET 8 SDK only + } EnsureDirectoryExists(artifactsForUnitTestsDir); DotNetTest(unitTestAssemblies, testSettings); - var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir).First().CombineWithFilePath(File("coverage.cobertura.xml")); + var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir) + .First() + .CombineWithFilePath(File("coverage.cobertura.xml")); Information(coverageSummaryFile); Information(artifactsForUnitTestsDir); @@ -396,11 +417,15 @@ Task("RunAcceptanceTests") var settings = new DotNetTestSettings { Configuration = compileConfig, + Framework = "net8.0", // .NET 8 SDK only ArgumentCustomization = args => args .Append("--no-restore") .Append("--no-build") }; - + if (target != Release) + { + settings.Framework = "net8.0"; // .NET 8 SDK only + } EnsureDirectoryExists(artifactsForAcceptanceTestsDir); DotNetTest(acceptanceTestAssemblies, settings); }); @@ -412,11 +437,15 @@ Task("RunIntegrationTests") var settings = new DotNetTestSettings { Configuration = compileConfig, + Framework = "net8.0", // .NET 8 SDK only ArgumentCustomization = args => args .Append("--no-restore") .Append("--no-build") }; - + if (target != Release) + { + settings.Framework = "net8.0"; // .NET 8 SDK only + } EnsureDirectoryExists(artifactsForIntegrationTestsDir); DotNetTest(integrationTestAssemblies, settings); }); diff --git a/docs/_static/overrides.css b/docs/_static/overrides.css new file mode 100644 index 000000000..a5f45454c --- /dev/null +++ b/docs/_static/overrides.css @@ -0,0 +1,6 @@ +blockquote { + font-size: 0.9em; +} +aside.footnote-list { + font-size: 0.9em; +} diff --git a/docs/conf.py b/docs/conf.py index 41b0fd36d..2c31a6b98 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'Ocelot' copyright = ' 2016-2024 ThreeMammals Ocelot team' author = 'Tom Pallister, Raman Maksimchuk and Ocelot Core team at ThreeMammals' -release = '23.2' +release = '23.3' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -27,3 +27,4 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_static_path html_static_path = ['_static'] +html_css_files = ['overrides.css'] diff --git a/docs/features/caching.rst b/docs/features/caching.rst index f2bf302ed..3d14b47c9 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -39,7 +39,8 @@ Finally, in order to use caching on a route in your Route configuration add this "FileCacheOptions": { "TtlSeconds": 15, "Region": "europe-central", - "Header": "Authorization" + "Header": "OC-Caching-Control", + "EnableContentHashing": false // my route has GET verb only, assigning 'true' for requests with body: POST, PUT etc. } In this example **TtlSeconds** is set to 15 which means the cache will expire after 15 seconds. @@ -48,10 +49,40 @@ The **Region** represents a region of caching. Additionally, if a header name is defined in the **Header** property, that header value is looked up by the key (header name) in the ``HttpRequest`` headers, and if the header is found, its value will be included in caching key. This causes the cache to become invalid due to the header value changing. +.. _cch-enablecontenthashing-option: + +``EnableContentHashing`` option +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In version `23.0`_, the new property **EnableContentHashing** has been introduced. Previously, the request body was utilized to compute the cache key. +However, due to potential performance issues arising from request body hashing, it has been disabled by default. +Clearly, this constitutes a breaking change and presents challenges for users who require cache key calculations that consider the request body (e.g., for the POST method). +To address this issue, it is recommended to enable the option either at the route level or globally in the :ref:`cch-global-configuration` section: + +.. code-block:: json + + "CacheOptions": { + // ... + "EnableContentHashing": true + } + +.. _cch-global-configuration: + +Global Configuration +-------------------- + +The positive update is that copying Route-level properties for each route is no longer necessary, as version `23.3`_ allows for setting their values in the ``GlobalConfiguration``. +This convenience extends to **Header** and **Region** as well. +However, an alternative is still being sought for **TtlSeconds**, which must be explicitly set for each route to enable caching. + +Notes +----- + If you look at the example `here `_ you can see how the cache manager is setup and then passed into the Ocelot ``AddCacheManager`` configuration method. You can use any settings supported by the **CacheManager** package and just pass them in. -Anyway, Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. You can also clear the cache for a region by calling Ocelot's administration API. +Anyway, Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. +You can also clear the cache for a region by calling Ocelot's administration API. Your Own Caching ---------------- @@ -68,3 +99,6 @@ If you want to add your own caching method, implement the following interfaces a Please dig into the Ocelot source code to find more. We would really appreciate it if anyone wants to implement `Redis `_, `Memcached `_ etc. Please, open a new `Show and tell `_ thread in `Discussions `_ space of the repository. + +.. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 552767f49..1be532e08 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -19,11 +19,14 @@ Here is an example Route configuration. You don't need to set all of these thing .. code-block:: json { - "DownstreamPathTemplate": "/", "UpstreamPathTemplate": "/", + "UpstreamHeaderTemplates": {}, // dictionary + "UpstreamHost": "", "UpstreamHttpMethod": [ "Get" ], + "DownstreamPathTemplate": "/", "DownstreamHttpMethod": "", "DownstreamHttpVersion": "", + "DownstreamHttpVersionPolicy": "", "AddHeadersToRequest": {}, "AddClaimsToRequest": {}, "RouteClaimsRequirement": {}, @@ -37,7 +40,7 @@ Here is an example Route configuration. You don't need to set all of these thing "ServiceName": "", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ - { "Host": "localhost", "Port": 51876 } + { "Host": "localhost", "Port": 12345 } ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 0, @@ -67,10 +70,12 @@ Here is an example Route configuration. You don't need to set all of these thing "IPAllowedList": [], "IPBlockedList": [], "ExcludeAllowedFromBlocked": false - } + }, + "Metadata": {} } -More information on how to use these options is below. +The actual Route schema for properties can be found in the C# `FileRoute `_ class. +If you're interested in learning more about how to utilize these options, read below! Multiple Environments --------------------- @@ -123,9 +128,16 @@ If you want to set the **GlobalConfiguration** property, you must have a file ca The way Ocelot merges the files is basically load them, loop over them, add any **Routes**, add any **AggregateRoutes** and if the file is called ``ocelot.global.json`` add the **GlobalConfiguration** aswell as any **Routes** or **AggregateRoutes**. Ocelot will then save the merged configuration to a file called `ocelot.json`_ and this will be used as the source of truth while Ocelot is running. -At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. -This is something to be aware of when you are investigating problems. -We would advise always checking what is in `ocelot.json`_ file if you have any problems. + **Note 1**: Currently, validation occurs only during the final merging of configurations in Ocelot. + It's essential to be aware of this when troubleshooting issues. + We recommend thoroughly inspecting the contents of the ``ocelot.json`` file if you encounter any problems. + + **Note 2**: The Merging feature is operational only during the application's startup. + Consequently, the merged configuration in ``ocelot.json`` remains static post-merging and startup. + It's important to be aware that the ``ConfigureAppConfiguration`` method is invoked solely during the startup of an ASP.NET web application. + Once the Ocelot application has started, you cannot call the ``AddOcelot`` method, nor can you employ the merging feature within ``AddOcelot``. + If you still require on-the-fly updating of the primary configuration file, ``ocelot.json``, please refer to the :ref:`config-react-to-changes` section. + Additionally, note that merging partial configuration files (such as ``ocelot.*.json``) on the fly using :doc:`../features/administration` API is not currently implemented. Keep files in a folder ^^^^^^^^^^^^^^^^^^^^^^ @@ -204,62 +216,14 @@ For example: Examining the code within the `ConfigurationBuilderExtensions class `_ would be helpful for gaining a better understanding of the signatures of the overloaded methods [#f2]_. -Store Configuration in Consul ------------------------------ - -The first thing you need to do is install the `NuGet package `_ that provides `Consul `_ support in Ocelot. - -.. code-block:: powershell - - Install-Package Ocelot.Provider.Consul - -Then you add the following when you register your services Ocelot will attempt to store and retrieve its configuration in Consul KV store. -In order to register Consul services we must call the ``AddConsul()`` and ``AddConfigStoredInConsul()`` extensions using the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f3]_ like below: - -.. code-block:: csharp +Store Configuration in `Consul`_ +-------------------------------- - services.AddOcelot() - .AddConsul() - .AddConfigStoredInConsul(); +As a developer, if you have enabled :doc:`../features/servicediscovery` with `Consul`_ support in Ocelot, you may choose to manage your configuration saving to the *Consul* `KV store`_. -You also need to add the following to your `ocelot.json`_. This is how Ocelot finds your Consul agent and interacts to load and store the configuration from Consul. - -.. code-block:: json - - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500 - } - } - -The team decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. -Why not take advantage of the fact Consul already gives you this! -We guess it means if you want to use Ocelot to its fullest, you take on Consul as a dependency for now. - -This feature has a `3 seconds `_ TTL cache before making a new request to your local Consul agent. - -.. _config-consul-key: - -Consul Configuration Key [#f4]_ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Beyond the traditional methods of storing configuration in a file vs folder (:ref:`config-merging-files`), or in-memory (:ref:`config-merging-tomemory`), you also have the alternative to utilize the `Consul`_ server's storage capabilities. -If you are using Consul for configuration (or other providers in the future), you might want to key your configurations: so you can have multiple configurations. - -In order to specify the key you need to set the **ConfigurationKey** property in the **ServiceDiscoveryProvider** options of the configuration JSON file e.g. - -.. code-block:: json - - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500, - "ConfigurationKey": "Ocelot_A" - } - } - -In this example Ocelot will use ``Ocelot_A`` as the key for your configuration when looking it up in Consul. -If you do not set the **ConfigurationKey**, Ocelot will use the string ``InternalConfiguration`` as the key. +For further details on managing Ocelot configurations via a Consul instance, please consult the ":ref:`sd-consul-configuration-in-kv`" section. Follow Redirects aka HttpHandlerOptions --------------------------------------- @@ -336,6 +300,8 @@ As a team, we highly recommend following these instructions when developing your System administrators or DevOps engineers must create real valid certificates being signed by hosting or cloud providers. **Switch off the feature for all routes!** Remove the **DangerousAcceptAnyServerCertificateValidator** property for all routes in production version of `ocelot.json`_ file! +.. _config-react-to-changes: + React to Configuration Changes ------------------------------ @@ -392,11 +358,76 @@ Registering a callback } } +.. _config-http-version: + DownstreamHttpVersion --------------------- Ocelot allows you to choose the HTTP version it will use to make the proxy request. It can be set as ``1.0``, ``1.1`` or ``2.0``. +* `HttpVersion Class `_ + +.. _config-version-policy: + +DownstreamHttpVersionPolicy [#f3]_ +---------------------------------- + +This routing property enables the configuration of the ``VersionPolicy`` property within ``HttpRequestMessage`` objects for downstream HTTP requests. +For additional details, refer to the following documentation: + +* `HttpRequestMessage.VersionPolicy Property `_ +* `HttpVersionPolicy Enum `_ +* `HttpVersion Class `_ + +The ``DownstreamHttpVersionPolicy`` option is intricately linked with the :ref:`config-http-version` setting. +Therefore, merely specifying ``DownstreamHttpVersion`` may sometimes be inadequate, particularly if your downstream services or Ocelot logs report HTTP connection errors such as ``PROTOCOL_ERROR``. +In these routes, selecting the precise ``DownstreamHttpVersionPolicy`` value is crucial for the ``HttpVersion`` policy to prevent such protocol errors. + +HTTP/2 version policy +^^^^^^^^^^^^^^^^^^^^^ + +**Given** you aim to ensure a smooth HTTP/2 connection setup for the Ocelot app and downstream services with SSL enabled: + +.. code-block:: json + + { + "DownstreamScheme": "https", + "DownstreamHttpVersion": "2.0", + "DownstreamHttpVersionPolicy": "", // empty + "DangerousAcceptAnyServerCertificateValidator": true + } + +**And** you configure global settings to use Kestrel with this snippet: + +.. code-block:: csharp + + var builder = WebApplication.CreateBuilder(args); + builder.WebHost.ConfigureKestrel(serverOptions => + { + serverOptions.ConfigureEndpointDefaults(listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + +**When** all components are set to communicate exclusively via HTTP/2 without TLS (plain HTTP). + +**Then** the downstream services may display error messages such as: + +.. code-block:: + + HTTP/2 connection error (PROTOCOL_ERROR): Invalid HTTP/2 connection preface + +To resolve the issue, ensure that ``HttpRequestMessage`` has its ``VersionPolicy`` set to ``RequestVersionOrHigher``. +Therefore, the ``DownstreamHttpVersionPolicy`` should be defined as follows: + +.. code-block:: json + + { + "DownstreamHttpVersion": "2.0", + "DownstreamHttpVersionPolicy": "RequestVersionOrHigher" // ! + } + Dependency Injection -------------------- @@ -416,14 +447,76 @@ You can utilize these methods in the ``ConfigureAppConfiguration`` method (locat You can find additional details in the dedicated :ref:`di-configuration-overview` section and in subsequent sections related to the :doc:`../features/dependencyinjection` chapter. +.. _config-route-metadata: + +Route Metadata +-------------- + +Ocelot provides various features such as routing, authentication, caching, load balancing, and more. However, some users may encounter situations where Ocelot does not meet their specific needs or they want to customize its behavior. In such cases, Ocelot allows users to add metadata to the route configuration. This property can store any arbitrary data that users can access in middlewares or delegating handlers. By using the metadata, users can implement their own logic and extend the functionality of Ocelot. + +Here is an example: + +.. code-block:: json + + { + "Routes": [ + { + "UpstreamHttpMethod": [ "GET" ], + "UpstreamPathTemplate": "/posts/{postId}", + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 80 } + ], + "Metadata": { + "api-id": "FindPost", + "my-extension/param1": "overwritten-value", + "other-extension/param1": "value1", + "other-extension/param2": "value2", + "tags": "tag1, tag2, area1, area2, func1", + "json": "[1, 2, 3, 4, 5]" + } + } + ], + "GlobalConfiguration": { + "Metadata": { + "instance_name": "dc-1-54abcz", + "my-extension/param1": "default-value" + } + } + } + +Now, the route metadata can be accessed through the `DownstreamRoute` object: + +.. code-block:: csharp + + public static class OcelotMiddlewares + { + public static Task PreAuthenticationMiddleware(HttpContext context, Func next) + { + var downstreamRoute = context.Items.DownstreamRoute(); + + if(downstreamRoute?.Metadata is {} metadata) + { + var param1 = metadata.GetValueOrDefault("my-extension/param1") ?? throw new MyExtensionException("Param 1 is null"); + var param2 = metadata.GetValueOrDefault("my-extension/param2", "custom-value"); + + // working with metadata + } + + return next(); + } + } + """" .. [#f1] ":ref:`config-merging-files`" feature was requested in `issue 296 `_, since then we extended it in `issue 1216 `_ (PR `1227 `_) as ":ref:`config-merging-tomemory`" subfeature which was released as a part of version `23.2`_. .. [#f2] ":ref:`config-merging-tomemory`" subfeature is based on the ``MergeOcelotJson`` enumeration type with values: ``ToFile`` and ``ToMemory``. The 1st one is implicit by default, and the second one is exactly what you need when merging to memory. See more details on implementations in the `ConfigurationBuilderExtensions`_ class. -.. [#f3] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. -.. [#f4] ":ref:`config-consul-key`" feature was requested in `issue 346 `_ as a part of version `7.0.0 `_. +.. [#f3] ":ref:`config-version-policy`" feature was requested in `issue 1672 `_ as a part of version `23.3`_. .. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 .. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json .. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +.. _Consul: https://www.consul.io/ +.. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv diff --git a/docs/features/kubernetes.rst b/docs/features/kubernetes.rst index 44411e589..8bbdb1881 100644 --- a/docs/features/kubernetes.rst +++ b/docs/features/kubernetes.rst @@ -73,7 +73,11 @@ The example here shows a typical configuration: } Service deployment in **Namespace** ``Dev``, **ServiceDiscoveryProvider** type is ``Kube``, you also can set :ref:`k8s-pollkube-provider` type. -Note: **Host**, **Port** and **Token** are no longer in use. + + **Note 1**: ``Host``, ``Port`` and ``Token`` are no longer in use. + + **Note 2**: The ``Kube`` provider searches for the service entry using ``ServiceName`` and then retrieves the first available port from the ``EndpointSubsetV1.Ports`` collection. + Therefore, if the port name is not specified, the default downstream scheme will be ``http``; .. _k8s-pollkube-provider: @@ -99,10 +103,10 @@ This really depends on how volatile your services are. We doubt it will matter for most people and polling may give a tiny performance improvement over calling Kubernetes per request. There is no way for Ocelot to work these out for you. -Global vs Route levels -^^^^^^^^^^^^^^^^^^^^^^ +Global vs Route Levels +---------------------- -If your downstream service resides in a different namespace, you can override the global setting at the Route-level by specifying a **ServiceNamespace**: +If your downstream service resides in a different namespace, you can override the global setting at the Route-level by specifying a ``ServiceNamespace``: .. code-block:: json @@ -113,7 +117,47 @@ If your downstream service resides in a different namespace, you can override th } ] +.. _k8s-downstream-scheme-vs-port-names: + +Downstream Scheme vs Port Names [#f3]_ +-------------------------------------- + +Kubernetes configuration permits the definition of multiple ports with names for each address of an endpoint subset. +When binding multiple ports, you assign a name to each subset port. +To allow the ``Kube`` provider to recognize the desired port by its name, you need to specify the ``DownstreamScheme`` with the port's name; +if not, the collection's first port entry will be chosen by default. + +For instance, consider a service on Kubernetes that exposes two ports: ``https`` for **443** and ``http`` for **80**, as follows: + +.. code-block:: text + + Name: my-service + Namespace: default + Subsets: + Addresses: 10.1.161.59 + Ports: + Name Port Protocol + ---- ---- -------- + https 443 TCP + http 80 TCP + +**When** you need to use the ``http`` port while intentionally bypassing the default ``https`` port (first one), +you must define ``DownstreamScheme`` to enable the provider to recognize the desired ``http`` port by comparing ``DownstreamScheme`` with the port name as follows: + +.. code-block:: json + + "Routes": [ + { + "ServiceName": "my-service", + "DownstreamScheme": "http", // port name -> http -> port is 80 + } + ] + +**Note**: In the absence of a specified ``DownstreamScheme`` (which is the default behavior), the ``Kube`` provider will select **the first available port** from the ``EndpointSubsetV1.Ports`` collection. +Consequently, if the port name is not designated, the default downstream scheme utilized will be ``http``. + """" .. [#f1] `Wikipedia `_ | `K8s Website `_ | `K8s Documentation `_ | `K8s GitHub `_ .. [#f2] This feature was requested as part of `issue 345 `_ to add support for `Kubernetes `_ :doc:`../features/servicediscovery` provider. +.. [#f3] *"Downstream Scheme vs Port Names"* feature was requested as part of `issue 1967 `_ and released in version `23.3 `_ diff --git a/docs/features/metadata.rst b/docs/features/metadata.rst new file mode 100644 index 000000000..902ff972e --- /dev/null +++ b/docs/features/metadata.rst @@ -0,0 +1,99 @@ +Metadata +======== + +Configuration +------------- + +Ocelot provides various features such as routing, authentication, caching, load +balancing, and more. +However, some users may encounter situations where Ocelot does not meet their +specific needs or they want to customize its behavior. +In such cases, Ocelot allows users to add metadata to the route configuration. +This property can store any arbitrary data that users can access in middlewares +or delegating handlers. + +By using the metadata, users can implement their own logic and extend the +functionality of Ocelot e.g. + +.. code-block:: json + + { + "Routes": [ + { + "UpstreamHttpMethod": [ "GET" ], + "UpstreamPathTemplate": "/posts/{postId}", + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 80 } + ], + "Metadata": { + "id": "FindPost", + "tags": "tag1, tag2, area1, area2, func1", + "plugin1.enabled": "true", + "plugin1.values": "[1, 2, 3, 4, 5]", + "plugin1.param": "value2", + "plugin1.param2": "123", + "plugin2/param1": "overwritten-value", + "plugin2/param2": "{\"name\":\"John Doe\",\"age\":30,\"city\":\"New York\",\"is_student\":false,\"hobbies\":[\"reading\",\"hiking\",\"cooking\"]}" + } + } + ], + "GlobalConfiguration": { + "Metadata": { + "instance_name": "machine-1", + "plugin2/param1": "default-value" + } + } + } + +Now, the route metadata can be accessed through the ``DownstreamRoute`` object: + +.. code-block:: csharp + + public class MyMiddleware + { + public Task Invoke(HttpContext context, Func next) + { + var route = context.Items.DownstreamRoute(); + var enabled = route.GetMetadata("plugin1.enabled"); + var values = route.GetMetadata("plugin1.values"); + var param1 = route.GetMetadata("plugin1.param", "system-default-value"); + var param2 = route.GetMetadata("plugin1.param2"); + + // working on the plugin1's function + + return next?.Invoke(); + } + } + +Extension Methods +----------------- + +Ocelot provides one DowstreamRoute extension method to help you retrieve your metadata values effortlessly. +With the exception of the types string, bool, bool?, string[] and numeric, all strings passed as parameters are treated as json strings and an attempt is made to convert them into objects of generic type T. +If the value is null, then, if not explicitely specified, the default for the chosen target type is returned. + +.. list-table:: + :widths: 20 40 40 + + * - Method + - Description + - Notes + * - ``GetMetadata`` + - The metadata value is returned as string without further parsing + - + * - ``GetMetadata`` + - The metadata value is splitted by a given separator (default ``,``) and returned as a string array. + - Several parameters can be set in the global configuration, such as Separators (default = ``[","]``), StringSplitOptions (default ``None``) and TrimChars, the characters that should be trimmed (default = ``[' ']``). + * - ``GetMetadata`` + - The metadata value is parsed to a number. + - Some parameters can be set in the global configuration, such as NumberStyle (default ``Any``) and CurrentCulture (default ``CultureInfo.CurrentCulture``) + * - ``GetMetadata`` + - The metadata value is converted to the given generic type. The value is treated as a json string and the json serializer tries to deserialize the string to the target type. + - A JsonSerializerOptions object can be passed as method parameter, Web is used as default. + * - ``GetMetadata`` + - Check if the metadata value is a truthy value, otherwise return false. + - The truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled`` + * - ``GetMetadata`` + - Check if the metadata value is a truthy value (return true), or falsy value (return false), otherwise return null. + - The known truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled``, ``1``, the known falsy values are: ``false``, ``no``, ``off``, ``disable``, ``disabled``, ``0`` diff --git a/docs/features/qualityofservice.rst b/docs/features/qualityofservice.rst index 799efdc08..fde699ddf 100644 --- a/docs/features/qualityofservice.rst +++ b/docs/features/qualityofservice.rst @@ -3,22 +3,31 @@ Quality of Service Label: `QoS `_ -Ocelot supports one QoS capability at the current time. You can set on a per Route basis if you want to use a circuit breaker when making requests to a downstream service. -This uses an awesome .NET library called `Polly`_, check them out `in official repository `_. +Ocelot currently supports a single **QoS** capability. +It allows you to configure, on a per-route basis, the use of a circuit breaker when making requests to downstream services. +This feature leverages a superb .NET library known as `Polly`_. For more information, visit their `official repository `_. -The first thing you need to do if you want to use the :doc:`../features/administration` API is bring in the relevant NuGet `package `_: +Installation +------------ + +To use the :doc:`../features/administration` API, the first step is to import the relevant NuGet `package `_: .. code-block:: powershell Install-Package Ocelot.Provider.Polly -Then in your ``ConfigureServices`` method to add `Polly`_ services we must call the ``AddPolly()`` extension of the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f1]_ like below: +Next, within your ``ConfigureServices`` method, to incorporate `Polly`_ services, invoke the ``AddPolly()`` extension on the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f1]_ as shown below: .. code-block:: csharp services.AddOcelot() .AddPolly(); +.. _qos-configuration: + +Configuration +------------- + Then add the following section to a Route configuration: .. code-block:: json @@ -29,44 +38,64 @@ Then add the following section to a Route configuration: "TimeoutValue": 5000 } -- You must set a number equal or greater than ``2`` against **ExceptionsAllowedBeforeBreaking** for this rule to be implemented. [#f2]_ -- **DurationOfBreak** means the circuit breaker will stay open for 1 second after it is tripped. -- **TimeoutValue** means if a request takes more than 5 seconds, it will automatically be timed out. +- You must set a number equal or greater than ``2`` against ``ExceptionsAllowedBeforeBreaking`` for this rule to be implemented. [#f2]_ +- ``DurationOfBreak`` means the circuit breaker will stay open for 1 second after it is tripped. +- ``TimeoutValue`` means if a request takes more than 5 seconds, it will automatically be timed out. -You can set the **TimeoutValue** in isolation of the **ExceptionsAllowedBeforeBreaking** and **DurationOfBreak** options: +.. _qos-circuit-breaker-strategy: + +Circuit Breaker strategy +------------------------ + +The options ``ExceptionsAllowedBeforeBreaking`` and ``DurationOfBreak`` can be configured independently of ``TimeoutValue``: .. code-block:: json "QoSOptions": { - "TimeoutValue": 5000 + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 1000 } -There is no point setting the other two in isolation as they affect each other! +Alternatively, you may omit ``DurationOfBreak`` to default to the implicit 5 seconds as per Polly `documentation `_: -Defaults --------- +.. code-block:: json -If you do not add a QoS section, QoS will not be used, however Ocelot will default to a **90** seconds timeout on all downstream requests. -If someone needs this to be configurable, open an issue. [#f2]_ + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3 + } -.. _qos-polly-v7-vs-v8: +This setup activates only the `Circuit breaker `_ strategy. -`Polly`_ v7 vs v8 ------------------ +.. _qos-timeout-strategy: -Important changes in version `23.2`_: [#f3]_ +Timeout strategy +---------------- - - With `Polly`_ version 8+, the ``ExceptionsAllowedBeforeBreaking`` value must be equal to or greater than **2**! - - The ``AddPolly`` method has been migrated from v7 policy wrappers to v8 resilience pipelines. Consequently, it now exhibits different behavior based on v8 pipelines. +The ``TimeoutValue`` can be configured independently from the ``ExceptionsAllowedBeforeBreaking`` and ``DurationOfBreak`` settings: -If you prefer not to modify your settings, you can continue using `Polly`_ v7 as follows: +.. code-block:: json -.. code-block:: csharp + "QoSOptions": { + "TimeoutValue": 5000 + } - services.AddOcelot() - .AddPollyV7(); +This setup activates only the `Timeout `_ strategy. + +Notes +----- + +1. Without a QoS section, QoS will not be utilized, and Ocelot will impose a default timeout of **90** seconds for all downstream requests. + To request configurability, please open an issue. [#f2]_ + +2. `Polly`_ V7 syntax is no longer supported as of version `23.2`_. [#f3]_ + +3. For `Polly`_ version 8 and above, the following constraints on values are specified in `the documentation `_: + + * The ``ExceptionsAllowedBeforeBreaking`` value must be **2** or higher. + * The ``DurationOfBreak`` value must exceed **500** milliseconds, defaulting to **5000** milliseconds (5 seconds) if unspecified or if the value is **500** milliseconds or less. + * The ``TimeoutValue`` must be over **10** milliseconds. -**Note**: Support for `Polly`_ v7 will be removed in a future version. We recommend avoiding this method (which is tagged as ``Obsolete``) unless absolutely necessary. + Consult the `Resilience strategies `_ documentation for a detailed understanding of each option. .. _qos-extensibility: diff --git a/docs/features/ratelimiting.rst b/docs/features/ratelimiting.rst index 9a69f4ded..94db1db5d 100644 --- a/docs/features/ratelimiting.rst +++ b/docs/features/ratelimiting.rst @@ -1,35 +1,50 @@ Rate Limiting ============= +`What's rate limiting? `_ + +* `Rate limiting | Wikipedia `_ +* `Rate Limiting pattern | Azure Architecture Center | Microsoft Learn `_ +* `Rate Limiting | Ask Google `_ + Ocelot Own Implementation ------------------------- -Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. +Ocelot provides *rate limiting* for upstream requests to prevent downstream services from becoming overwhelmed. [#f1]_ -The authors of this feature were inspired by `@catcherwong article `_ to finally write this documentation. -This feature was added by `@geffzhang `_ on GitHub! Thanks very much! +Rate Limit by Client's Header +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To get rate limiting working for a Route you need to add the following JSON to it: +To implement *rate limiting* for a Route, you need to incorporate the following JSON configuration: .. code-block:: json "RateLimitOptions": { - "ClientWhitelist": [], + "ClientWhitelist": [], // array of strings "EnableRateLimiting": true, - "Period": "1s", - "PeriodTimespan": 1, + "Period": "1s", // seconds, minutes, hours, days + "PeriodTimespan": 1, // only seconds "Limit": 1 } -* **ClientWhitelist** - This is an array that contains the whitelist of the client. - It means that the client in this array will not be affected by the rate limiting. -* **EnableRateLimiting** - This value specifies enable endpoint rate limiting. -* **Period** - This value specifies the period that the limit applies to, such as ``1s``, ``5m``, ``1h``, ``1d`` and so on. - If you make more requests in the period than the limit allows then you need to wait for **PeriodTimespan** to elapse before you make another request. -* **PeriodTimespan** - This value specifies that we can retry after a certain number of seconds. -* **Limit** - This value specifies the maximum number of requests that a client can make in a defined period. +* **ClientWhitelist** - An array containing the whitelisted clients. Clients listed here will be exempt from rate limiting. + For more information on the **ClientIdHeader** option, refer to the :ref:`rl-global-configuration` section. +* **EnableRateLimiting** - This setting enables rate limiting on endpoints. +* **Period** - This parameter defines the duration for which the limit is applicable, such as ``1s`` (seconds), ``5m`` (minutes), ``1h`` (hours), and ``1d`` (days). + If you reach the exact **Limit** of requests, the excess occurs immediately, and the **PeriodTimespan** begins. + You must wait for the **PeriodTimespan** duration to pass before making another request. + Should you exceed the number of requests within the period more than the **Limit** permits, the **QuotaExceededMessage** will appear in the response, accompanied by the **HttpStatusCode**. +* **PeriodTimespan** - This parameter indicates the time in **seconds** after which a retry is permissible. + During this interval, the **QuotaExceededMessage** will appear in the response, accompanied by an **HttpStatusCode**. + Clients are advised to consult the ``Retry-After`` header to determine the timing of subsequent requests. +* **Limit** - This parameter defines the upper limit of requests a client is allowed to make within a specified **Period**. + +.. _rl-global-configuration: + +Global Configuration +^^^^^^^^^^^^^^^^^^^^ -You can also set the following in the **GlobalConfiguration** part of **ocelot.json**: +You can set the following in the ``GlobalConfiguration`` section of `ocelot.json`_: .. code-block:: json @@ -38,33 +53,48 @@ You can also set the following in the **GlobalConfiguration** part of **ocelot.j "RateLimitOptions": { "DisableRateLimitHeaders": false, "QuotaExceededMessage": "Customize Tips!", - "HttpStatusCode": 123, - "ClientIdHeader": "Test" + "HttpStatusCode": 418, // I'm a teapot + "ClientIdHeader": "MyRateLimiting" } } -* **DisableRateLimitHeaders** - This value specifies whether ``X-Rate-Limit`` and ``Retry-After`` headers are disabled. -* **QuotaExceededMessage** - This value specifies the exceeded message. -* **HttpStatusCode** - This value specifies the returned HTTP status code when rate limiting occurs. -* **ClientIdHeader** - Allows you to specifiy the header that should be used to identify clients. By default it is ``ClientId`` +* **DisableRateLimitHeaders** - Determines if the ``X-Rate-Limit`` and ``Retry-After`` headers are disabled. +* **QuotaExceededMessage** - Defines the message displayed when the quota is exceeded. It is optional and the default message is informative. +* **HttpStatusCode** - Indicates the HTTP status code returned during *rate limiting*. The default value is **429** (`Too Many Requests`_). +* **ClientIdHeader** - Specifies the header used to identify clients, with ``ClientId`` as the default. Future and ASP.NET Core Implementation -------------------------------------- -The Ocelot team considers to redesign *Rate Limiting* feature, -because of `Announcing Rate Limiting for .NET `_ by Brennan Conroy on July 13th, 2022. -There is no decision at the moment, and the old version of the feature is included as a part of release `20.0 `_ for .NET 7. +The Ocelot team is contemplating a redesign of the *Rate Limiting* feature following the `Announcing Rate Limiting for .NET`_ by Brennan Conroy on July 13th, 2022. +Currently, no decision has been made, and the previous version of the feature remains part of the `20.0`_ release for .NET 7. [#f2]_ -See more about new feature being added into ASP.NET Core 7.0 release: +Discover the new features being introduced in the ASP.NET Core 7.0 release: -* `RateLimiter Class `_, since ASP.NET Core **7.0** -* `System.Threading.RateLimiting `_ NuGet package -* `Rate limiting middleware in ASP.NET Core `_ article by Arvin Kahbazi, Maarten Balliauw, and Rick Anderson +* The `RateLimiter Class `_, available since ASP.NET Core 7.0 +* The `System.Threading.RateLimiting `_ NuGet package +* The `Rate limiting middleware in ASP.NET Core `_ article by Arvin Kahbazi, Maarten Balliauw, and Rick Anderson -However, it makes sense to keep the old implementation as a Ocelot built-in native feature, but we are going to migrate to the new Rate Limiter from ``Microsoft.AspNetCore.RateLimiting`` namespace. +While retaining the old implementation as an Ocelot built-in feature makes sense, we plan to transition to the new Rate Limiter from the ``Microsoft.AspNetCore.RateLimiting`` namespace. +Please share your thoughts with us in the `Discussions `_ space of the repository. |octocat| + +"""" + +.. [#f1] Historically, the *"Ocelot Own Rate Limiting"* feature is one of the oldest and first features of Ocelot. This feature was delivered in PR `37`_ by `@geffzhang`_ on GitHub. Many thanks! It was initially released in version `1.3.2`_. The authors were inspired by `@catcherwong article`_ to write this documentation. +.. [#f2] Since PR `37`_ and version `1.3.2`_, the Ocelot team has reviewed and redesigned the feature to provide stable behavior. The fix for bug `1590`_ (PR `1592`_) was released as part of version `23.3`_. + +.. _Announcing Rate Limiting for .NET: https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/ +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _@geffzhang: https://github.com/ThreeMammals/Ocelot/commits?author=geffzhang +.. _@catcherwong article: http://www.c-sharpcorner.com/article/building-api-gateway-using-ocelot-in-asp-net-core-rate-limiting-part-four/ +.. _Too Many Requests: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 +.. _37: https://github.com/ThreeMammals/Ocelot/pull/37 +.. _1590: https://github.com/ThreeMammals/Ocelot/issues/1590 +.. _1592: https://github.com/ThreeMammals/Ocelot/pull/1592 +.. _1.3.2: https://github.com/ThreeMammals/Ocelot/releases/tag/1.3.2 +.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png :alt: octocat :width: 23 - -Please, share your opinion to us in the `Discussions `_ space of the repository. |octocat| diff --git a/docs/features/requestaggregation.rst b/docs/features/requestaggregation.rst index f1123cf19..c9a642388 100644 --- a/docs/features/requestaggregation.rst +++ b/docs/features/requestaggregation.rst @@ -217,9 +217,12 @@ Below is an example of an aggregator that you could implement for your solution: Gotchas ------- -You cannot use Routes with specific **RequestIdKeys** as this would be crazy complicated to track. +* You cannot use Routes with specific **RequestIdKeys** as this would be crazy complicated to track. +* Aggregation only supports the ``GET`` HTTP verb. +* Aggregation allows for the forwarding of ``HttpRequest.Body`` to downstream services by duplicating the body data. + Form data and attached files should also be forwarded. + It is essential to always specify the ``Content-Length`` header in requests to upstream; otherwise, Ocelot will log warnings like *"Aggregation does not support body copy without Content-Length header!"*. -Aggregation only supports the ``GET`` HTTP verb. """" diff --git a/docs/features/routing.rst b/docs/features/routing.rst index 6de03d8e2..825f5d9e1 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -154,11 +154,63 @@ The Route above will only be matched when the ``Host`` header value is ``somedom If you do not set **UpstreamHost** on a Route then any ``Host`` header will match it. This means that if you have two Routes that are the same, apart from the **UpstreamHost**, where one is null and the other set Ocelot will favour the one that has been set. +.. _routing-upstream-headers: + +Upstream Headers [#f3]_ +----------------------- + +In addition to routing by ``UpstreamPathTemplate``, you can also define ``UpstreamHeaderTemplates``. +For a route to match, all headers specified in this dictionary object must be present in the request headers. + +.. code-block:: json + + { + // ... + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": [ "Get" ], + "UpstreamHeaderTemplates": { // dictionary + "country": "uk", // 1st header + "version": "v1" // 2nd header + } + } + +In this scenario, the route will only match if a request includes both headers with the specified values. + +Header placeholders +^^^^^^^^^^^^^^^^^^^ + +Let's explore a more intriguing scenario where placeholders can be effectively utilized within your ``UpstreamHeaderTemplates``. + +Consider the following approach using the special placeholder format ``{header:placeholdername}``: + +.. code-block:: json + + { + "DownstreamPathTemplate": "/{versionnumber}/api", // with placeholder + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { "Host": "10.0.10.1", "Port": 80 } + ], + "UpstreamPathTemplate": "/api", + "UpstreamHttpMethod": [ "Get" ], + "UpstreamHeaderTemplates": { + "version": "{header:versionnumber}" // 'header:' prefix vs placeholder + } + } + +In this scenario, the entire value of the request header "**version**" is inserted into the ``DownstreamPathTemplate``. +If necessary, a more intricate upstream header template can be specified, using placeholders such as ``version-{header:version}_country-{header:country}``. + + **Note 1**: Placeholders are not required in ``DownstreamPathTemplate``. + This scenario can be utilized to mandate a specific header regardless of its value. + + **Note 2**: Additionally, the ``UpstreamHeaderTemplates`` dictionary options are applicable for :doc:`../features/requestaggregation` as well. + Priority -------- You can define the order you want your Routes to match the Upstream ``HttpRequest`` by including a **Priority** property in **ocelot.json**. -See `issue 270 `_ for reference. +See issue `270`_ for reference. .. code-block:: json @@ -195,7 +247,7 @@ Query String Placeholders In addition to URL path :ref:`routing-placeholders` Ocelot is able to forward query string parameters with their processing in the form of ``{something}``. Also, the query parameter placeholder needs to be present in both the **DownstreamPathTemplate** and **UpstreamPathTemplate** properties. -Placeholder replacement works bi-directionally between path and query strings, with some `restrictions <#restrictions-on-use>`_ on usage. +Placeholder replacement works bi-directionally between path and query strings, with some restrictions on usage (see :ref:`routing-merging-of-query-parameters`). Path to Query String direction ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -209,7 +261,9 @@ Ocelot allows you to specify a query string as part of the **DownstreamPathTempl "DownstreamPathTemplate": "/api/subscriptions/{subscription}/updates?unitId={unit}", } -In this example Ocelot will use the value from the ``{unit}`` placeholder in the upstream path template and add it to the downstream request as a query string parameter called ``unitId``! Make sure you name the placeholder differently due to `restrictions <#restrictions-on-use>`_ on usage. +In this example Ocelot will use the value from the ``{unit}`` placeholder in the upstream path template and add it to the downstream request as a query string parameter called ``unitId``! + + Note! Make sure you name the placeholder differently due to :ref:`routing-merging-of-query-parameters`. Query String to Path direction @@ -227,7 +281,10 @@ Ocelot will also allow you to put query string parameters in the **UpstreamPathT In this example Ocelot will only match requests that have a matching URL path and the query string starts with ``unitId=something``. You can have other queries after this but you must start with the matching parameter. Also Ocelot will swap the ``{uid}`` parameter from the query string and use it in the downstream request path. -Note, the best practice is giving different placeholder name than the name of query parameter due to `restrictions <#restrictions-on-use>`_ on usage. + + Note, the best practice is giving different placeholder name than the name of query parameter due to :ref:`routing-merging-of-query-parameters`. + +.. _routing-catch-all-query-string: Catch All Query String ^^^^^^^^^^^^^^^^^^^^^^ @@ -243,58 +300,72 @@ The placeholder ``{everything}`` name does not matter, any name will work. } This entire query string routing feature is very useful in cases where the query string should not be transformed but rather routed without any changes, -such as OData filters and etc (see issue `1174 `_). +such as OData filters and etc (see issue `1174`_). -**Note**, the ``{everything}`` placeholder can be empty while catching all query strings, because this is a part of the :ref:`routing-empty-placeholders` feature! [#f1]_ -Thus, upstream paths ``/contracts?`` and ``/contracts`` are routed to downstream path ``/apipath/contracts``, which has no query string at all. + **Note**, the ``{everything}`` placeholder can be empty while catching all query strings, because this is a part of the :ref:`routing-empty-placeholders` feature! [#f1]_ + Thus, upstream paths ``/contracts?`` and ``/contracts`` are routed to downstream path ``/apipath/contracts``, which has no query string at all. -Restrictions on use -^^^^^^^^^^^^^^^^^^^ +.. _routing-merging-of-query-parameters: + +Merging of Query Parameters +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Query string parameters are unsorted and merged to create the final downstream URL. +This process is essential as the ``DownstreamUrlCreatorMiddleware`` requires control over placeholder replacement and the merging of duplicate parameters. +A parameter that appears first in the **UpstreamPathTemplate** may occupy a different position in the final downstream URL. +Furthermore, if the **DownstreamPathTemplate** includes query parameters at the beginning, their position in the **UpstreamPathTemplate** will be indeterminate unless explicitly defined. + +In a typical scenario, the merging algorithm constructs the final downstream URL query string by: -The query string parameters are ordered and merged to produce the final downstream URL. -This is necessary because the ``DownstreamUrlCreatorMiddleware`` needs to have some control when replacing placeholders and merging duplicate parameters. -So, even if your parameter is presented as the first parameter in the upstream, then in the final downstream URL the said query parameter will have a different position. -But this doesn't seem to break anything in the downstream API. +1. Taking the initially defined query parameters in **DownstreamPathTemplate** and placing them at the beginning, with any necessary placeholder replacements. +2. Adding all parameters from the :ref:`routing-catch-all-query-string`, represented by the placeholder ``{everything}``, into the second position (following the explicitly defined parameters from **step 1**). +3. Appending any remaining replaced placeholder values as parameter values to the end of the string, if they are present. -Because of parameters merging, special ASP.NET API `model binding `_ -for arrays is not supported if you use array items representation like ``selectedCourses=1050&selectedCourses=2000``. -This query string will be merged as ``selectedCourses=1050`` in downstream URL. So, array data will be lost! -Make sure upstream clients generate correct query string for array models like ``selectedCourses[0]=1050&selectedCourses[1]=2000``. -To understand array model bidings, see `Bind arrays and string values from headers and query strings `_ docs. +Array parameters in ASP.NET API's model binding +""""""""""""""""""""""""""""""""""""""""""""""" -**Warning!** Query string placeholders have naming restrictions due to ``DownstreamUrlCreatorMiddleware`` implementations. -On the other hand, it gives you the flexibility to control whether the parameter is present in the final downstream URL. -Here are two user scenarios. +Due to parameters merging, ASP.NET API's special `model binding`_ for arrays **is not supported** having the array item representation format of ``selectedCourses=1050&selectedCourses=2000``. +This query string will be merged into ``selectedCourses=1050`` in the downstream URL, resulting in the loss of array data. +It is crucial for upstream clients to generate the correct query string for array models, such as ``selectedCourses[0]=1050&selectedCourses[1]=2000``. +For a comprehensive understanding of array model bindings, refer to the documentation: `Bind arrays and string values from headers and query strings`_. -* User wants to save the parameter after replacing the placeholder (see issue `473 `_). - To do this you need to use the following template definition: +Control over parameter existence +"""""""""""""""""""""""""""""""" - .. code-block:: json +Be aware that query string placeholders are subject to naming restrictions due to the ``DownstreamUrlCreatorMiddleware``'s merging algorithm implementation. +However, this also provides the flexibility to manage the presence of parameters in the final downstream URL by their names. + +Consider the following 2 development scenarios :htm:`→` + +1. A developer wishes **to preserve a parameter** after substituting a placeholder (refer to issue `473`_). + This requires the use of the template definition below: + + .. code-block:: json - { - "UpstreamPathTemplate": "/path/{serverId}/{action}", - "DownstreamPathTemplate": "/path2/{action}?server={serverId}" - } + { + "UpstreamPathTemplate": "/path/{serverId}/{action}", + "DownstreamPathTemplate": "/path2/{action}?server={serverId}" + } - So, ``{serverId}`` placeholder and ``server`` parameter **names are different**! - Finally, the ``server`` parameter is kept. + | Here, the ``{serverId}`` placeholder and the ``server`` parameter **names differ**! Ultimately, the ``server`` parameter is retained. + | It is important to note that due to the case-sensitive comparison of names, the ``server`` parameter will not be preserved with the ``{server}`` placeholder. However, using the ``{Server}`` placeholder is acceptable for retaining the parameter. -* User wants to remove old parameter after replacing placeholder (see issue `952 `_). - To do this you need to use the same names: +2. The developer intends **to remove an outdated parameter** after substituting a placeholder (refer to issue `952`_). + For this action, you must use identical names having the case-sensitive comparison: - .. code-block:: json + .. code-block:: json - { - "UpstreamPathTemplate": "/users?userId={userId}", - "DownstreamPathTemplate": "/persons?personId={userId}" - } + { + "UpstreamPathTemplate": "/users?userId={userId}", + "DownstreamPathTemplate": "/persons?personId={userId}" + } - So, both ``{userId}`` placeholder and ``userId`` parameter **names are the same**! - Finally, the ``userId`` parameter is removed. + | Thus, the ``{userId}`` placeholder and the ``userId`` parameter **have identical names**! Subsequently, the ``userId`` parameter is eliminated. + | Be aware that due to the case sensitive nature of the comparison, if the ``{userid}`` placeholder is used, the ``userId`` parameter will not be removed! .. _routing-security-options: -Security Options [#f3]_ +Security Options [#f4]_ ----------------------- Ocelot allows you to manage multiple patterns for allowed/blocked IPs using the `IPAddressRange `_ package @@ -326,7 +397,7 @@ The current patterns managed are the following: .. _routing-dynamic: -Dynamic Routing [#f4]_ +Dynamic Routing [#f5]_ ---------------------- The idea is to enable dynamic routing when using a :doc:`../features/servicediscovery` provider so you don't have to provide the Route config. @@ -336,5 +407,13 @@ See the :ref:`sd-dynamic-routing` docs if this sounds interesting to you. .. [#f1] ":ref:`routing-empty-placeholders`" feature is available starting in version `23.0 `_, see issue `748 `_ and the `23.0 `__ release notes for details. .. [#f2] ":ref:`routing-upstream-host`" feature was requested as part of `issue 216 `_. -.. [#f3] ":ref:`routing-security-options`" feature was requested as part of `issue 628 `_ (of `12.0.1 `_ version), then redesigned and improved by `issue 1400 `_, and published in version `20.0 `_ docs. -.. [#f4] ":ref:`routing-dynamic`" feature was requested as part of `issue 340 `_. Complete reference: :ref:`sd-dynamic-routing`. +.. [#f3] ":ref:`routing-upstream-headers`" feature was proposed in `issue 360 `_, and released in version `24.0 `_. +.. [#f4] ":ref:`routing-security-options`" feature was requested as part of `issue 628 `_ (of `12.0.1 `_ version), then redesigned and improved by `issue 1400 `_, and published in version `20.0 `_ docs. +.. [#f5] ":ref:`routing-dynamic`" feature was requested as part of `issue 340 `_. Complete reference: :ref:`sd-dynamic-routing`. + +.. _model binding: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-8.0#collections +.. _Bind arrays and string values from headers and query strings: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding?view=aspnetcore-8.0#bind-arrays-and-string-values-from-headers-and-query-strings +.. _270: https://github.com/ThreeMammals/Ocelot/issues/270 +.. _473: https://github.com/ThreeMammals/Ocelot/issues/473 +.. _952: https://github.com/ThreeMammals/Ocelot/issues/952 +.. _1174: https://github.com/ThreeMammals/Ocelot/issues/1174 diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index 6d9d8d4ef..a4e4f96c5 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -9,28 +9,85 @@ At the moment this is only supported in the **GlobalConfiguration** section, whi Consul ------ - | **Namespace**: `Ocelot.Provider.Consul `_ + | **Namespace**: ``Ocelot.Provider.Consul`` -The first thing you need to do is install the `Ocelot.Provider.Consul `__ package that provides `Consul `_ support in Ocelot: +The first thing you need to do is install the `Ocelot.Provider.Consul `_ package that provides `Consul`_ support in Ocelot: .. code-block:: powershell Install-Package Ocelot.Provider.Consul -Then add the following to your ``ConfigureServices`` method: +To register *Consul* services, you must invoke the ``AddConsul()`` extension using the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f1]_. +Therefore, include the following in your ``ConfigureServices`` method: .. code-block:: csharp services.AddOcelot() - .AddConsul(); + .AddConsul(); // or .AddConsul() -Currently there are 2 types of Consul *service discovery* providers: ``Consul`` and ``PollConsul``. -The default provider is ``Consul``, which means that if ``ConsulProviderFactory`` cannot read, understand, or parse the **Type** property of the ``ServiceProviderConfiguration`` object, then a ``Consul`` provider instance is created by the factory. +Currently there are 2 types of *Consul* service discovery providers: ``Consul`` and ``PollConsul``. +The default provider is ``Consul``, which means that if ``ConsulProviderFactory`` cannot read, understand, or parse the **Type** property of the ``ServiceProviderConfiguration`` object, +then a :ref:`sd-consul-provider` instance is created by the factory. -Explore these types of providers and understand the differences in the subsections below. +Explore these types of providers and understand the differences in the subsections: :ref:`sd-consul-provider` and :ref:`sd-pollconsul-provider`. -Consul Provider Type -^^^^^^^^^^^^^^^^^^^^ +.. _sd-consul-configuration-in-kv: + +Configuration in `KV Store`_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Add the following when you register your services Ocelot will attempt to store and retrieve its :doc:`../features/configuration` in *Consul* `KV Store`_: + +.. code-block:: csharp + + services.AddOcelot() + .AddConsul() + .AddConfigStoredInConsul(); // ! + +You also need to add the following to your `ocelot.json`_. +This is how Ocelot finds your *Consul* agent and interacts to load and store the configuration from *Consul*. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500 + } + } + +The team decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. +Why not take advantage of the fact Consul already gives you this! +We guess it means if you want to use Ocelot to its fullest, you take on Consul as a dependency for now. + + **Note!** This feature has a `3 seconds TTL`_ cache before making a new request to your local *Consul* agent. + +.. _sd-consul-configuration-key: + +Consul Configuration Key [#f2]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you are using *Consul* for :doc:`../features/configuration` (or other providers in the future), you might want to key your configurations: so you can have multiple configurations. + +In order to specify the key you need to set the **ConfigurationKey** property in the **ServiceDiscoveryProvider** options of the configuration JSON file e.g. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500, + "ConfigurationKey": "Ocelot_A" // ! + } + } + +In this example Ocelot will use ``Ocelot_A`` as the key for your configuration when looking it up in *Consul*. +If you do not set the **ConfigurationKey**, Ocelot will use the string ``InternalConfiguration`` as the key. + +.. _sd-consul-provider: + +``Consul`` Provider +^^^^^^^^^^^^^^^^^^^ | **Class**: `Ocelot.Provider.Consul.Consul `_ @@ -67,8 +124,10 @@ If no load balancer is specified, Ocelot will not load balance requests. When this is set up Ocelot will lookup the downstream host and port from the *service discovery* provider and load balance requests across any available services. -PollConsul Provider Type -^^^^^^^^^^^^^^^^^^^^^^^^ +.. _sd-pollconsul-provider: + +``PollConsul`` Provider +^^^^^^^^^^^^^^^^^^^^^^^ | **Class**: `Ocelot.Provider.Consul.PollConsul `_ @@ -98,7 +157,7 @@ Service Definition Your services need to be added to Consul something like below (C# style but hopefully this make sense)... The only important thing to note is not to add ``http`` or ``https`` to the ``Address`` field. We have been contacted before about not accepting scheme in ``Address``. -After reading `this `_ we do not think the scheme should be in there. +After reading `Agents Overview `_ and `Define services `_ docs we do not think the **scheme** should be in there. In C# @@ -140,6 +199,68 @@ In order so this to work you must add the additional property below: Ocelot will add this token to the Consul client that it uses to make requests and that is then used for every request. +.. _sd-consul-service-builder: + +Consul Service Builder [#f3]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + | **Interface**: ``IConsulServiceBuilder`` + | **Implementation**: ``DefaultConsulServiceBuilder`` + +The Ocelot community has consistently reported, both in the past and presently, issues with *Consul* services (such as connectivity) due to a variety of *Consul* agent definitions. +Some DevOps engineers prefer to group services as *Consul* `catalog nodes`_ by customizing the assignment of host names to node names, +while others focus on defining agent services with pure IP addresses as hosts, which relates to the `954`_ bug dilemma. + +Since version `13.5.2`_, the building of service downstream host/port in PR `909`_ has been altered to favor the node name as the host over the agent service address IP. + +Version `23.3`_ saw the introduction of a customization feature that allows control over the service building process through the ``DefaultConsulServiceBuilder`` class. +This class has virtual methods that can be overridden to meet the needs of developers and DevOps. + +The present logic in the ``DefaultConsulServiceBuilder`` class is as follows: + +.. code-block:: csharp + + protected virtual string GetDownstreamHost(ServiceEntry entry, Node node) + => node != null ? node.Name : entry.Service.Address; + +Some DevOps engineers choose to ignore node names, opting instead for abstract identifiers rather than actual hostnames. +Our team, however, advocates for the assignment of real hostnames or IP addresses to node names, upholding this as a best practice. +If this approach does not align with your needs, or if you prefer not to spend time detailing your nodes for downstream services, you might consider defining agent services without node names. +In such cases within a *Consul* setup, you would need to override the behavior of the ``DefaultConsulServiceBuilder`` class. +For further details, refer to the subsequent section below. + +.. _sd-addconsul-generic-method: + +``AddConsul`` method +""""""""""""""""""""""" + + | **Signature**: ``IOcelotBuilder AddConsul(this IOcelotBuilder builder)`` + +Overriding the ``DefaultConsulServiceBuilder`` behavior involves two steps: defining a new class that inherits from the ``IConsulServiceBuilder`` interface, +and then injecting this new behavior into DI using the ``AddConsul`` helper. +However, the quickest and most streamlined approach is to inherit directly from the ``DefaultConsulServiceBuilder`` class, which offers greater flexibility. + +**First**, we need to define a new service building class: + +.. code-block:: csharp + + public class MyConsulServiceBuilder : DefaultConsulServiceBuilder + { + public MyConsulServiceBuilder(Func configurationFactory, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) + : base(configurationFactory, clientFactory, loggerFactory) { } + // I want to use the agent service IP address as the downstream hostname + protected override string GetDownstreamHost(ServiceEntry entry, Node node) => entry.Service.Address; + } + +**Second**, we must inject the new behavior into DI, as demonstrated in the Ocelot versus Consul setup: + +.. code-block:: csharp + + services.AddOcelot() + .AddConsul(); + +You can refer to `the acceptance test`_ in the repository for an example. + Eureka ------ @@ -410,3 +531,22 @@ After this, you need to add the ``IServiceDiscoveryProviderFactory`` interface t Note that in this case the Ocelot pipeline will not use ``ServiceDiscoveryProviderFactory`` by default. Additionally, you do not need to specify ``"Type": "MyServiceDiscoveryProvider"`` in the **ServiceDiscoveryProvider** properties of the **GlobalConfiguration** settings. But you can leave this ``Type`` option for compatibility between both designs. + +"""" + +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. +.. [#f2] *"Consul Configuration Key"* feature was requested in issue `346`_ as a part of version `7.0.0`_. +.. [#f3] Customization of *"Consul Service Builder"* was implemented as a part of bug `954`_ fixing and the feature was delivered in version `23.3`_. + +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _Consul: https://www.consul.io/ +.. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv +.. _3 seconds TTL: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+TimeSpan.FromSeconds%283%29&type=code +.. _catalog nodes: https://developer.hashicorp.com/consul/api-docs/catalog#list-nodes +.. _the acceptance test: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+Should_return_service_address_by_overridden_service_builder_when_there_is_a_node&type=code +.. _346: https://github.com/ThreeMammals/Ocelot/issues/346 +.. _909: https://github.com/ThreeMammals/Ocelot/pull/909 +.. _954: https://github.com/ThreeMammals/Ocelot/issues/954 +.. _7.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.0 +.. _13.5.2: https://github.com/ThreeMammals/Ocelot/releases/tag/13.5.2 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 diff --git a/docs/index.rst b/docs/index.rst index be0b6deb4..d208ae6d3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,13 +1,49 @@ .. _Polly: https://github.com/App-vNext/Polly -.. _@ebjornset: https://github.com/ebjornset -.. _@RaynaldM: https://github.com/RaynaldM -.. _@ArwynFr: https://github.com/ArwynFr -.. _@AlyHKafoury: https://github.com/AlyHKafoury -.. _@FelixBoers: https://github.com/FelixBoers -.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 +.. _Circuit Breaker: https://www.pollydocs.org/strategies/circuit-breaker.html +.. _Timeout: https://www.pollydocs.org/strategies/timeout.html -Welcome to Ocelot `23.2`_ -====================================================================================== +.. _@raman-m: https://github.com/raman-m +.. _@RaynaldM: https://github.com/RaynaldM +.. _@jlukawska: https://github.com/jlukawska +.. _@ibnuda: https://github.com/ibnuda +.. _@vantm: https://github.com/vantm +.. _@sergio-str: https://github.com/sergio-str +.. _@PaulARoy: https://github.com/PaulARoy +.. _@thiagoloureiro: https://github.com/thiagoloureiro +.. _@bbenameur: https://github.com/bbenameur + +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 +.. _23.3.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 +.. _23.2.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 + +.. _954: https://github.com/ThreeMammals/Ocelot/issues/954 +.. _957: https://github.com/ThreeMammals/Ocelot/issues/957 +.. _1026: https://github.com/ThreeMammals/Ocelot/issues/1026 +.. _1312: https://github.com/ThreeMammals/Ocelot/pull/1312 +.. _1590: https://github.com/ThreeMammals/Ocelot/issues/1590 +.. _1592: https://github.com/ThreeMammals/Ocelot/pull/1592 +.. _1673: https://github.com/ThreeMammals/Ocelot/pull/1673 +.. _1843: https://github.com/ThreeMammals/Ocelot/pull/1843 +.. _2002: https://github.com/ThreeMammals/Ocelot/issues/2002 +.. _2003: https://github.com/ThreeMammals/Ocelot/pull/2003 +.. _2034: https://github.com/ThreeMammals/Ocelot/issues/2034 +.. _2039: https://github.com/ThreeMammals/Ocelot/issues/2039 +.. _2045: https://github.com/ThreeMammals/Ocelot/pull/2045 +.. _2050: https://github.com/ThreeMammals/Ocelot/pull/2050 +.. _2052: https://github.com/ThreeMammals/Ocelot/pull/2052 +.. _2054: https://github.com/ThreeMammals/Ocelot/discussions/2054 +.. _2058: https://github.com/ThreeMammals/Ocelot/pull/2058 +.. _2059: https://github.com/ThreeMammals/Ocelot/issues/2059 +.. _2067: https://github.com/ThreeMammals/Ocelot/pull/2067 +.. _2079: https://github.com/ThreeMammals/Ocelot/pull/2079 +.. _2085: https://github.com/ThreeMammals/Ocelot/issues/2085 +.. _2086: https://github.com/ThreeMammals/Ocelot/pull/2086 + +.. role:: htm(raw) + :format: html + +Welcome to Ocelot `23.3`_ +========================= Thanks for taking a look at the Ocelot documentation! Please use the left hand navigation to get around. The team would suggest taking a look at the **Introduction** chapter first. @@ -20,67 +56,108 @@ We **do** follow development process which is described in :doc:`../building/rel Release Notes ------------- - | **Release Tag**: `23.2.0 `_ - | **Release Codename**: `Lunar Eclipse `_ +| Release Tag: `23.3.0`_ +| Release Codename: **Twilight Texas** + :htm:`→` `for men `_ + :htm:`→` `for women `_ + :htm:`→` `for black men `_ What's new? ^^^^^^^^^^^ -- :doc:`../features/configuration`: A brand new :ref:`config-merging-tomemory` by `@ebjornset`_ as a part of the :ref:`config-merging-files` feature. - - The ``AddOcelot`` method merges the **ocelot.*.json** files into a single **ocelot.json** file as the primary configuration file, which is written back to disk and then added to the ``IConfigurationBuilder`` for the well-known ``IConfiguration``. You can now call another ``AddOcelot`` method that adds the merged JSON directly from memory to the ``IConfigurationBuilder``, using ``AddJsonStream`` instead. - - See more details in :ref:`di-configuration-overview` of :doc:`../features/dependencyinjection`. +- :doc:`../features/servicediscovery`: Introducing a new feature for "*Customization of services creation*" in two primary service discovery providers: ``Consul`` (:ref:`sd-consul-service-builder`) and ``Kubernetes`` (:ref:`k8s-downstream-scheme-vs-port-names`), developed by `@raman-m`_. + + The customization for both ``Consul`` and ``Kube`` providers in service creation is achieved through the overriding of virtual methods in default implementations. The recommendation was to separate the provider's logic and introduce ``public virtual`` and ``protected virtual`` methods in concrete classes, enabling: + + - The use of ``public virtual`` methods as dictated by interface definitions. + - The application of ``protected virtual`` methods to allow developers to customize atomic operations through inheritance from existing concrete classes. + - The injection of new interface objects into the provider's constructor. + - The overriding of the default behavior of classes. + + | Ultimately, customization relies on the virtual methods within the default implementation classes, providing developers the flexibility to override them as necessary for highly tailored Consul/K8s configurations in their specific environments. + | For further details, refer to the respective pull requests for both providers: + + - ``Kube`` :htm:`→` PR `2052`_ + - ``Consul`` :htm:`→` PR `2067`_ + +- :doc:`../features/routing`: Introducing the new ":ref:`routing-upstream-headers`" feature by `@jlukawska`_. + + | In addition to routing via ``UpstreamPathTemplate``, you can now define an ``UpstreamHeaderTemplates`` options dictionary. For a route to match, all headers specified in this section are required to be present in the request headers. + | For more details, see PR `1312`_. + +- :doc:`../features/configuration`: Introducing the ":ref:`config-version-policy`" feature by `@ibnuda`_. + + The configurable ``HttpRequestMessage.VersionPolicy`` helps avoid HTTP protocol connection errors and stabilizes connections to downstream services, especially when you're not developing those services, documentation is scarce, or the deployed HTTP protocol version is uncertain. + For developers of downstream services, it's possible to ``ConfigureKestrel`` server and its endpoints with new protocol settings. However, attention to version policy is also required, and this feature provides precise version settings for HTTP connections. -- :doc:`../features/servicefabric`: Published old undocumented :ref:`sf-placeholders` feature of :doc:`../features/servicefabric` `service discovery provider `_. + | Essentially, this feature promotes the use of HTTP protocols beyond 1.0/1.1, such as HTTP/2 or even HTTP/3. + | For additional details, refer to PR `1673`_. - This feature by `@FelixBoers`_ is available starting from version `13.0.0 `_. +- :doc:`../features/configuration`: Introducing the new ":ref:`config-route-metadata`" feature by `@vantm`_. -- :doc:`../features/qualityofservice`: A brand new `Polly`_ v8 pipelines :ref:`qos-extensibility` feature by `@RaynaldM`_ + Undoubtedly, this is the standout feature of the release! ⭐ + + Route metadata enables Ocelot developers to incorporate custom functions that address specific needs or to create their own plugins/extensions. + + In versions of Ocelot prior to `23.3.0`_, the configuration was limited to predefined values that Ocelot used internally. This was sufficient for official extensions, but posed challenges for third-party developers who needed to implement configurations not included in the standard ``FileConfiguration``. + Applying an option to a specific route required knowledge of the array index and other details that might not be readily accessible using the standard ``IConfiguration`` or ``IOptions`` models from ASP.NET. + + | Now, :doc:`../features/metadata` can be directly accessed in the ``DownstreamRoute`` object. Furthermore, metadata can also be retrieved from the global JSON section via the ``FileConfiguration.GlobalConfiguration`` property. + | For more information, see the details in PR `1843`_ on this remarkable feature. Focus On ^^^^^^^^ Updates of the features """"""""""""""""""""""" - - - :doc:`../features/configuration`: New :ref:`config-merging-tomemory` feature by `@ebjornset`_ - - :doc:`../features/dependencyinjection`: Added new overloaded :ref:`di-configuration-addocelot` by `@ebjornset`_ - - :doc:`../features/qualityofservice`: Support of new `Polly`_ v8 syntax and new :ref:`qos-extensibility` feature by `@RaynaldM`_ + +- :doc:`../features/configuration`: New features are ":ref:`config-version-policy`" by `@ibnuda`_ and ":ref:`config-route-metadata`" by `@vantm`_. +- :doc:`../features/servicediscovery`: New feature is "*Customization of services creation*" aka :ref:`sd-consul-service-builder` and :ref:`k8s-downstream-scheme-vs-port-names` by `@raman-m`_. +- :doc:`../features/routing`: New feature is ":ref:`routing-upstream-headers`" by `@jlukawska`_. +- :doc:`../features/qualityofservice`: The team has decided to remove the Polly V7 policies logic and the corresponding Ocelot ``AddPollyV7`` extensions (referenced in PR `2079`_). + + | Furthermore, the Polly V8 Circuit Breaker has been mandated as the primary strategy (as per PR `2086`_). + | See more detaild below in "**Ocelot extra packages**" paragraph. Ocelot extra packages """"""""""""""""""""" - - `Ocelot.Provider.Polly `_: Support of new `Polly`_ v8 syntax. +- `Ocelot.Provider.Polly `_ - | *Polly* `8.0+ `_ versions introduced the concept of `resilience pipelines `_. - | All `AddPolly extensions `_ have been automatically migrated from **v7** to **v8**. - | Please note that older **v7** extensions are marked with the ``[Obsolete]`` attribute and renamed using the ``V7`` suffix. And the old **v7** implementation has been moved to the `v7 namespace `_. - | See more details in :ref:`qos-polly-v7-vs-v8` section of :doc:`../features/qualityofservice` chapter. + - Our team has resolved to eliminate the Polly V7 policies logic and the corresponding Ocelot ``AddPollyV7`` extensions entirely (refer to the "`Polly v7 vs v8 `_" documentation). + In the previous `23.2.0`_ release, named `Lunar Eclipse `_, we included these to maintain the legacy `Polly`_ behavior, allowing development teams to transition or retain the old Polly V7 functionality. + We are now confident that it is time to progress alongside `Polly`_, shifting our focus to the new `Polly V8 `_ `resilience pipelines `_. + For more details, see PR `2079`_. + - Additionally, we have implemented Polly v8 `Circuit Breaker `_ as the primary strategy. + Our :doc:`../features/qualityofservice` (QoS) relies on two main strategies: :ref:`qos-circuit-breaker-strategy` and :ref:`qos-timeout-strategy`. + If both `Circuit Breaker`_ and `Timeout`_ have :ref:`qos-configuration` with their respective properties in the ``QoSOptions`` of the route JSON, then the :ref:`qos-circuit-breaker-strategy` will take precedence in the constructed resilience pipeline. + For more details, refer to PR `2086`_. Stabilization aka bug fixing """""""""""""""""""""""""""" - - `683 `_ by PR `1927 `_. Thanks to `@AlyHKafoury`_! +- Fixed `2034`_ in PR `2045`_ by `@raman-m`_ +- Fixed `2039`_ in PR `2050`_ by `@PaulARoy`_ +- Fixed `1590`_ in PR `1592`_ by `@sergio-str`_ +- Fixed `2054`_ `2059`_ in PR `2058`_ by `@thiagoloureiro`_ +- Fixed `954`_ `957`_ `1026`_ in PR `2067`_ by `@raman-m`_ +- Fixed `2002`_ in PR `2003`_ by `@bbenameur`_ +- Fixed `2085`_ in PR `2086`_ by `@RaynaldM`_ - | `New rules `_ have been added to Ocelot's configuration validation logic to find duplicate placeholders in path templates. - | See more in the `FileConfigurationFluentValidator `_ class. +See `all bugs `_ of the `Spring'24 `_ milestone - - `1518 `_ hotfix by PR `1986 `_. Thanks to `@ArwynFr`_! - - | Using the default ``IServiceCollection`` `DI extensions `_ to register Ocelot services resulted in the ``ServiceCollection`` provider being forced to be created by calling ``BuildServiceProvider()``. - | This resulted in problems with dependency injection libraries, or worse, causing the Ocelot app to crash! - | See more in the `ServiceCollectionExtensions `_ class. - - - See `all bugs `_ of the `February'24 `_ milestone - -Updated Documentation -""""""""""""""""""""" +Documentation for version `23.3`_ +""""""""""""""""""""""""""""""""" - - :doc:`../features/configuration` - - :doc:`../features/dependencyinjection` - - :doc:`../features/qualityofservice` - - :doc:`../features/servicefabric` +- :doc:`../features/caching`: New :ref:`cch-enablecontenthashing-option` and :ref:`cch-global-configuration` sections +- :doc:`../features/configuration`: New :ref:`config-version-policy` and :ref:`config-route-metadata` sections +- :doc:`../features/kubernetes`: New :ref:`k8s-downstream-scheme-vs-port-names` section +- :doc:`../features/metadata`: This is new chapter for :ref:`config-route-metadata` feature +- :doc:`../features/qualityofservice` +- :doc:`../features/ratelimiting` +- :doc:`../features/requestaggregation` +- :doc:`../features/routing`: New :ref:`routing-upstream-headers` section +- :doc:`../features/servicediscovery`: New :ref:`sd-consul-service-builder` and :ref:`k8s-downstream-scheme-vs-port-names` sections .. toctree:: @@ -113,6 +190,7 @@ Updated Documentation features/kubernetes features/loadbalancer features/logging + features/metadata features/methodtransformation features/middlewareinjection features/qualityofservice diff --git a/docs/make.bat b/docs/make.bat old mode 100755 new mode 100644 diff --git a/samples/AdministrationApi/Issue645.postman_collection.json b/samples/Administration/Issue645.postman_collection.json similarity index 97% rename from samples/AdministrationApi/Issue645.postman_collection.json rename to samples/Administration/Issue645.postman_collection.json index c9bac89f5..6fedd16b9 100644 --- a/samples/AdministrationApi/Issue645.postman_collection.json +++ b/samples/Administration/Issue645.postman_collection.json @@ -1,150 +1,150 @@ -{ - "info": { - "_postman_id": "6234b40a-e363-4c73-8577-1c9074abb951", - "name": "Issue645", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "1. GET http://localhost: 55580/administration/.well-known/openid-configuration", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{AccessToken}}" - } - ], - "body": {}, - "url": { - "raw": "http://localhost:5000/administration/.well-known/openid-configuration", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - ".well-known", - "openid-configuration" - ] - } - }, - "response": [] - }, - { - "name": "3. GET http://localhost: 55580/administration/configuration", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{AccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"routes\": [\r\n {\r\n \"downstreamPathTemplate\": \"/{everything}\",\r\n \"upstreamPathTemplate\": \"/templates/{everything}\",\r\n \"upstreamHttpMethod\": [\r\n \"GET\"\r\n ],\r\n \"addHeadersToRequest\": {},\r\n \"upstreamHeaderTransform\": {},\r\n \"downstreamHeaderTransform\": {},\r\n \"addClaimsToRequest\": {},\r\n \"routeClaimsRequirement\": {},\r\n \"addQueriesToRequest\": {},\r\n \"requestIdKey\": null,\r\n \"fileCacheOptions\": {\r\n \"ttlSeconds\": 0,\r\n \"region\": null\r\n },\r\n \"routeIsCaseSensitive\": false,\r\n \"downstreamScheme\": \"http\",\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"rateLimitOptions\": {\r\n \"clientWhitelist\": [],\r\n \"enableRateLimiting\": false,\r\n \"period\": null,\r\n \"periodTimespan\": 0,\r\n \"limit\": 0\r\n },\r\n \"authenticationOptions\": {\r\n \"authenticationProviderKey\": null,\r\n \"allowedScopes\": []\r\n },\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n },\r\n \"downstreamHostAndPorts\": [\r\n {\r\n \"host\": \"localhost\",\r\n \"port\": 50689\r\n }\r\n ],\r\n \"upstreamHost\": null,\r\n \"key\": null,\r\n \"delegatingHandlers\": [],\r\n \"priority\": 1,\r\n \"timeout\": 0,\r\n \"dangerousAcceptAnyServerCertificateValidator\": false\r\n }\r\n ],\r\n \"aggregates\": [],\r\n \"globalConfiguration\": {\r\n \"requestIdKey\": \"Request-Id\",\r\n \"rateLimitOptions\": {\r\n \"clientIdHeader\": \"ClientId\",\r\n \"quotaExceededMessage\": null,\r\n \"rateLimitCounterPrefix\": \"ocelot\",\r\n \"disableRateLimitHeaders\": false,\r\n \"httpStatusCode\": 429\r\n },\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"baseUrl\": \"http://localhost:55580\",\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"downstreamScheme\": null,\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n }\r\n }\r\n}" - }, - "url": { - "raw": "http://localhost:5000/administration/configuration", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - "configuration" - ] - } - }, - "response": [] - }, - { - "name": "2. POST http://localhost: 55580/administration/connect/token", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var jsonData = JSON.parse(responseBody);", - "postman.setGlobalVariable(\"AccessToken\", jsonData.access_token);", - "postman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);" - ] - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "client_id", - "value": "admin", - "type": "text" - }, - { - "key": "client_secret", - "value": "secret", - "type": "text" - }, - { - "key": "scope", - "value": "admin", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - } - ] - }, - "url": { - "raw": "http://localhost:5000/administration/connect/token", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - "connect", - "token" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "0f60e7b3-e4f1-4458-bbc4-fc4809e86b2d", - "type": "text/javascript", - "exec": [ - string.Empty - ] - } - }, - { - "listen": "test", - "script": { - "id": "1279a2cf-b771-4a86-9dfa-302b240fac62", - "type": "text/javascript", - "exec": [ - string.Empty - ] - } - } - ] +{ + "info": { + "_postman_id": "6234b40a-e363-4c73-8577-1c9074abb951", + "name": "Issue645", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "1. GET http://localhost: 55580/administration/.well-known/openid-configuration", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{AccessToken}}" + } + ], + "body": {}, + "url": { + "raw": "http://localhost:5000/administration/.well-known/openid-configuration", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + ".well-known", + "openid-configuration" + ] + } + }, + "response": [] + }, + { + "name": "3. GET http://localhost: 55580/administration/configuration", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{AccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"routes\": [\r\n {\r\n \"downstreamPathTemplate\": \"/{everything}\",\r\n \"upstreamPathTemplate\": \"/templates/{everything}\",\r\n \"upstreamHttpMethod\": [\r\n \"GET\"\r\n ],\r\n \"addHeadersToRequest\": {},\r\n \"upstreamHeaderTransform\": {},\r\n \"downstreamHeaderTransform\": {},\r\n \"addClaimsToRequest\": {},\r\n \"routeClaimsRequirement\": {},\r\n \"addQueriesToRequest\": {},\r\n \"requestIdKey\": null,\r\n \"fileCacheOptions\": {\r\n \"ttlSeconds\": 0,\r\n \"region\": null\r\n },\r\n \"routeIsCaseSensitive\": false,\r\n \"downstreamScheme\": \"http\",\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"rateLimitOptions\": {\r\n \"clientWhitelist\": [],\r\n \"enableRateLimiting\": false,\r\n \"period\": null,\r\n \"periodTimespan\": 0,\r\n \"limit\": 0\r\n },\r\n \"authenticationOptions\": {\r\n \"authenticationProviderKey\": null,\r\n \"allowedScopes\": []\r\n },\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n },\r\n \"downstreamHostAndPorts\": [\r\n {\r\n \"host\": \"localhost\",\r\n \"port\": 50689\r\n }\r\n ],\r\n \"upstreamHost\": null,\r\n \"key\": null,\r\n \"delegatingHandlers\": [],\r\n \"priority\": 1,\r\n \"timeout\": 0,\r\n \"dangerousAcceptAnyServerCertificateValidator\": false\r\n }\r\n ],\r\n \"aggregates\": [],\r\n \"globalConfiguration\": {\r\n \"requestIdKey\": \"Request-Id\",\r\n \"rateLimitOptions\": {\r\n \"clientIdHeader\": \"ClientId\",\r\n \"quotaExceededMessage\": null,\r\n \"rateLimitCounterPrefix\": \"ocelot\",\r\n \"disableRateLimitHeaders\": false,\r\n \"httpStatusCode\": 429\r\n },\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"baseUrl\": \"http://localhost:55580\",\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"downstreamScheme\": null,\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:5000/administration/configuration", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + "configuration" + ] + } + }, + "response": [] + }, + { + "name": "2. POST http://localhost: 55580/administration/connect/token", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "postman.setGlobalVariable(\"AccessToken\", jsonData.access_token);", + "postman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "client_id", + "value": "admin", + "type": "text" + }, + { + "key": "client_secret", + "value": "secret", + "type": "text" + }, + { + "key": "scope", + "value": "admin", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + } + ] + }, + "url": { + "raw": "http://localhost:5000/administration/connect/token", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + "connect", + "token" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "0f60e7b3-e4f1-4458-bbc4-fc4809e86b2d", + "type": "text/javascript", + "exec": [ + string.Empty + ] + } + }, + { + "listen": "test", + "script": { + "id": "1279a2cf-b771-4a86-9dfa-302b240fac62", + "type": "text/javascript", + "exec": [ + string.Empty + ] + } + } + ] } \ No newline at end of file diff --git a/samples/AdministrationApi/AdministrationApi.csproj b/samples/Administration/Ocelot.Samples.AdministrationApi.csproj similarity index 100% rename from samples/AdministrationApi/AdministrationApi.csproj rename to samples/Administration/Ocelot.Samples.AdministrationApi.csproj diff --git a/samples/AdministrationApi/Program.cs b/samples/Administration/Program.cs similarity index 100% rename from samples/AdministrationApi/Program.cs rename to samples/Administration/Program.cs diff --git a/samples/AdministrationApi/Properties/launchSettings.json b/samples/Administration/Properties/launchSettings.json similarity index 100% rename from samples/AdministrationApi/Properties/launchSettings.json rename to samples/Administration/Properties/launchSettings.json diff --git a/samples/AdministrationApi/README.md b/samples/Administration/README.md similarity index 96% rename from samples/AdministrationApi/README.md rename to samples/Administration/README.md index 42a01b2f4..59d236aa7 100644 --- a/samples/AdministrationApi/README.md +++ b/samples/Administration/README.md @@ -1,94 +1,94 @@ -```json -{ - "routes": [ - { - "downstreamPathTemplate": "/{everything}", - "upstreamPathTemplate": "/templates/{everything}", - "upstreamHttpMethod": [ - "GET" - ], - "addHeadersToRequest": {}, - "upstreamHeaderTransform": {}, - "downstreamHeaderTransform": {}, - "addClaimsToRequest": {}, - "routeClaimsRequirement": {}, - "addQueriesToRequest": {}, - "requestIdKey": null, - "fileCacheOptions": { - "ttlSeconds": 0, - "region": null - }, - "routeIsCaseSensitive": false, - "downstreamScheme": "http", - "qoSOptions": { - "exceptionsAllowedBeforeBreaking": 0, - "durationOfBreak": 0, - "timeoutValue": 0 - }, - "loadBalancerOptions": { - "type": null, - "key": null, - "expiry": 0 - }, - "rateLimitOptions": { - "clientWhitelist": [], - "enableRateLimiting": false, - "period": null, - "periodTimespan": 0, - "limit": 0 - }, - "authenticationOptions": { - "authenticationProviderKey": null, - "allowedScopes": [] - }, - "httpHandlerOptions": { - "allowAutoRedirect": false, - "useCookieContainer": false, - "useTracing": false, - "useProxy": true - }, - "downstreamHostAndPorts": [ - { - "host": "localhost", - "port": 50689 - } - ], - "upstreamHost": null, - "key": null, - "delegatingHandlers": [], - "priority": 1, - "timeout": 0, - "dangerousAcceptAnyServerCertificateValidator": false - } - ], - "aggregates": [], - "globalConfiguration": { - "requestIdKey": "Request-Id", - "rateLimitOptions": { - "clientIdHeader": "ClientId", - "quotaExceededMessage": null, - "rateLimitCounterPrefix": "ocelot", - "disableRateLimitHeaders": false, - "httpStatusCode": 429 - }, - "qoSOptions": { - "exceptionsAllowedBeforeBreaking": 0, - "durationOfBreak": 0, - "timeoutValue": 0 - }, - "baseUrl": "http://localhost:55580", - "loadBalancerOptions": { - "type": null, - "key": null, - "expiry": 0 - }, - "downstreamScheme": null, - "httpHandlerOptions": { - "allowAutoRedirect": false, - "useCookieContainer": false, - "useTracing": false, - "useProxy": true - } - } -} -``` +```json +{ + "routes": [ + { + "downstreamPathTemplate": "/{everything}", + "upstreamPathTemplate": "/templates/{everything}", + "upstreamHttpMethod": [ + "GET" + ], + "addHeadersToRequest": {}, + "upstreamHeaderTransform": {}, + "downstreamHeaderTransform": {}, + "addClaimsToRequest": {}, + "routeClaimsRequirement": {}, + "addQueriesToRequest": {}, + "requestIdKey": null, + "fileCacheOptions": { + "ttlSeconds": 0, + "region": null + }, + "routeIsCaseSensitive": false, + "downstreamScheme": "http", + "qoSOptions": { + "exceptionsAllowedBeforeBreaking": 0, + "durationOfBreak": 0, + "timeoutValue": 0 + }, + "loadBalancerOptions": { + "type": null, + "key": null, + "expiry": 0 + }, + "rateLimitOptions": { + "clientWhitelist": [], + "enableRateLimiting": false, + "period": null, + "periodTimespan": 0, + "limit": 0 + }, + "authenticationOptions": { + "authenticationProviderKey": null, + "allowedScopes": [] + }, + "httpHandlerOptions": { + "allowAutoRedirect": false, + "useCookieContainer": false, + "useTracing": false, + "useProxy": true + }, + "downstreamHostAndPorts": [ + { + "host": "localhost", + "port": 50689 + } + ], + "upstreamHost": null, + "key": null, + "delegatingHandlers": [], + "priority": 1, + "timeout": 0, + "dangerousAcceptAnyServerCertificateValidator": false + } + ], + "aggregates": [], + "globalConfiguration": { + "requestIdKey": "Request-Id", + "rateLimitOptions": { + "clientIdHeader": "ClientId", + "quotaExceededMessage": null, + "rateLimitCounterPrefix": "ocelot", + "disableRateLimitHeaders": false, + "httpStatusCode": 429 + }, + "qoSOptions": { + "exceptionsAllowedBeforeBreaking": 0, + "durationOfBreak": 0, + "timeoutValue": 0 + }, + "baseUrl": "http://localhost:55580", + "loadBalancerOptions": { + "type": null, + "key": null, + "expiry": 0 + }, + "downstreamScheme": null, + "httpHandlerOptions": { + "allowAutoRedirect": false, + "useCookieContainer": false, + "useTracing": false, + "useProxy": true + } + } +} +``` diff --git a/samples/AdministrationApi/appsettings.json b/samples/Administration/appsettings.json similarity index 100% rename from samples/AdministrationApi/appsettings.json rename to samples/Administration/appsettings.json diff --git a/samples/AdministrationApi/ocelot.json b/samples/Administration/ocelot.json similarity index 95% rename from samples/AdministrationApi/ocelot.json rename to samples/Administration/ocelot.json index 0fa4143ff..02e7c5512 100644 --- a/samples/AdministrationApi/ocelot.json +++ b/samples/Administration/ocelot.json @@ -1,18 +1,18 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/service/stats/collected", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 5100 - } - ], - "UpstreamPathTemplate": "/api/stats/collected" - } - ], - "GlobalConfiguration": { - "BaseUrl": "http://localhost:5000" - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/service/stats/collected", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5100 + } + ], + "UpstreamPathTemplate": "/api/stats/collected" + } + ], + "GlobalConfiguration": { + "BaseUrl": "http://localhost:5000" + } } \ No newline at end of file diff --git a/samples/AdministrationApi/tempkey.rsa b/samples/Administration/tempkey.rsa similarity index 100% rename from samples/AdministrationApi/tempkey.rsa rename to samples/Administration/tempkey.rsa diff --git a/samples/OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj b/samples/Basic/Ocelot.Samples.Basic.ApiGateway.csproj similarity index 100% rename from samples/OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj rename to samples/Basic/Ocelot.Samples.Basic.ApiGateway.csproj diff --git a/samples/OcelotBasic/Program.cs b/samples/Basic/Program.cs similarity index 97% rename from samples/OcelotBasic/Program.cs rename to samples/Basic/Program.cs index 34c555f19..3901407c2 100644 --- a/samples/OcelotBasic/Program.cs +++ b/samples/Basic/Program.cs @@ -1,29 +1,29 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.IO; +using System.IO; -namespace Ocelot.Samples.OcelotBasic.ApiGateway; - -public class Program -{ - public static void Main(string[] args) - { - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json") - .AddEnvironmentVariables(); - }) - .ConfigureLogging((hostingContext, logging) => +namespace Ocelot.Samples.OcelotBasic.ApiGateway; + +public class Program +{ + public static void Main(string[] args) + { + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") + .AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => { if (hostingContext.HostingEnvironment.IsDevelopment()) { @@ -31,10 +31,10 @@ public static void Main(string[] args) logging.AddConsole(); } //add your logging - }) - .UseIISIntegration() + }) + .UseIISIntegration() .UseStartup() - .Build() - .Run(); - } -} + .Build() + .Run(); + } +} diff --git a/samples/OcelotBasic/Properties/launchSettings.json b/samples/Basic/Properties/launchSettings.json similarity index 100% rename from samples/OcelotBasic/Properties/launchSettings.json rename to samples/Basic/Properties/launchSettings.json diff --git a/samples/OcelotBasic/Startup.cs b/samples/Basic/Startup.cs similarity index 100% rename from samples/OcelotBasic/Startup.cs rename to samples/Basic/Startup.cs diff --git a/samples/OcelotBasic/appsettings.Development.json b/samples/Basic/appsettings.Development.json similarity index 100% rename from samples/OcelotBasic/appsettings.Development.json rename to samples/Basic/appsettings.Development.json diff --git a/samples/OcelotKube/ApiGateway/appsettings.json b/samples/Basic/appsettings.json similarity index 100% rename from samples/OcelotKube/ApiGateway/appsettings.json rename to samples/Basic/appsettings.json diff --git a/samples/OcelotBasic/ocelot.json b/samples/Basic/ocelot.json similarity index 95% rename from samples/OcelotBasic/ocelot.json rename to samples/Basic/ocelot.json index 2864550cd..7cab02430 100644 --- a/samples/OcelotBasic/ocelot.json +++ b/samples/Basic/ocelot.json @@ -1,21 +1,21 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/todos/{id}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 443 - } - ], - "UpstreamPathTemplate": "/posts/{id}", - "UpstreamHttpMethod": [ - "Get" - ] - } - ], - "GlobalConfiguration": { - "BaseUrl": "https://localhost:5000" - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/todos/{id}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/posts/{id}", + "UpstreamHttpMethod": [ + "Get" + ] + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://localhost:5000" + } } diff --git a/samples/Docker/README.md b/samples/Docker/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/samples/OcelotEureka/ApiGateway/ApiGateway.csproj b/samples/Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj similarity index 100% rename from samples/OcelotEureka/ApiGateway/ApiGateway.csproj rename to samples/Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj diff --git a/samples/OcelotEureka/ApiGateway/Program.cs b/samples/Eureka/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotEureka/ApiGateway/Program.cs rename to samples/Eureka/ApiGateway/Program.cs diff --git a/samples/OcelotEureka/ApiGateway/Properties/launchSettings.json b/samples/Eureka/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotEureka/ApiGateway/Properties/launchSettings.json rename to samples/Eureka/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotEureka/ApiGateway/appsettings.json b/samples/Eureka/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotEureka/ApiGateway/appsettings.json rename to samples/Eureka/ApiGateway/appsettings.json diff --git a/samples/OcelotEureka/ApiGateway/ocelot.json b/samples/Eureka/ApiGateway/ocelot.json similarity index 96% rename from samples/OcelotEureka/ApiGateway/ocelot.json rename to samples/Eureka/ApiGateway/ocelot.json index 5a69973de..747cf23c8 100644 --- a/samples/OcelotEureka/ApiGateway/ocelot.json +++ b/samples/Eureka/ApiGateway/ocelot.json @@ -1,22 +1,22 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/api/Category", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/Category", - "ServiceName": "ncore-rat", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10000, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "AdministrationPath": "/administration", - "ServiceDiscoveryProvider": { "Type": "Eureka" } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/Category", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/Category", + "ServiceName": "ncore-rat", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10000, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "AdministrationPath": "/administration", + "ServiceDiscoveryProvider": { "Type": "Eureka" } + } +} diff --git a/samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs b/samples/Eureka/DownstreamService/Controllers/CategoryController.cs similarity index 100% rename from samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs rename to samples/Eureka/DownstreamService/Controllers/CategoryController.cs diff --git a/samples/OcelotEureka/DownstreamService/DownstreamService.csproj b/samples/Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj similarity index 100% rename from samples/OcelotEureka/DownstreamService/DownstreamService.csproj rename to samples/Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj diff --git a/samples/OcelotEureka/DownstreamService/Program.cs b/samples/Eureka/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotEureka/DownstreamService/Program.cs rename to samples/Eureka/DownstreamService/Program.cs diff --git a/samples/OcelotEureka/DownstreamService/Properties/launchSettings.json b/samples/Eureka/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotEureka/DownstreamService/Properties/launchSettings.json rename to samples/Eureka/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotEureka/DownstreamService/Startup.cs b/samples/Eureka/DownstreamService/Startup.cs similarity index 100% rename from samples/OcelotEureka/DownstreamService/Startup.cs rename to samples/Eureka/DownstreamService/Startup.cs diff --git a/samples/OcelotEureka/DownstreamService/appsettings.Development.json b/samples/Eureka/DownstreamService/appsettings.Development.json similarity index 100% rename from samples/OcelotEureka/DownstreamService/appsettings.Development.json rename to samples/Eureka/DownstreamService/appsettings.Development.json diff --git a/samples/OcelotEureka/DownstreamService/appsettings.json b/samples/Eureka/DownstreamService/appsettings.json similarity index 100% rename from samples/OcelotEureka/DownstreamService/appsettings.json rename to samples/Eureka/DownstreamService/appsettings.json diff --git a/samples/OcelotEureka/OcelotEureka.sln b/samples/Eureka/OcelotEureka.sln similarity index 100% rename from samples/OcelotEureka/OcelotEureka.sln rename to samples/Eureka/OcelotEureka.sln diff --git a/samples/OcelotEureka/README.md b/samples/Eureka/README.md similarity index 100% rename from samples/OcelotEureka/README.md rename to samples/Eureka/README.md diff --git a/samples/OcelotGraphQL/OcelotGraphQL.csproj b/samples/GraphQL/Ocelot.Samples.GraphQL.csproj similarity index 100% rename from samples/OcelotGraphQL/OcelotGraphQL.csproj rename to samples/GraphQL/Ocelot.Samples.GraphQL.csproj diff --git a/samples/OcelotGraphQL/OcelotGraphQL.sln b/samples/GraphQL/OcelotGraphQL.sln similarity index 100% rename from samples/OcelotGraphQL/OcelotGraphQL.sln rename to samples/GraphQL/OcelotGraphQL.sln diff --git a/samples/OcelotGraphQL/Program.cs b/samples/GraphQL/Program.cs similarity index 96% rename from samples/OcelotGraphQL/Program.cs rename to samples/GraphQL/Program.cs index e519875f1..e2f19aaaf 100644 --- a/samples/OcelotGraphQL/Program.cs +++ b/samples/GraphQL/Program.cs @@ -1,134 +1,134 @@ -using GraphQL; -using GraphQL.Types; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace OcelotGraphQL -{ - public class Hero - { - public int Id { get; set; } - public string Name { get; set; } - } - - public class Query - { - private readonly List _heroes = new() - { - new Hero { Id = 1, Name = "R2-D2" }, - new Hero { Id = 2, Name = "Batman" }, - new Hero { Id = 3, Name = "Wonder Woman" }, - new Hero { Id = 4, Name = "Tom Pallister" } - }; - - [GraphQLMetadata("hero")] - public Hero GetHero(int id) - { - return _heroes.FirstOrDefault(x => x.Id == id); - } - } - - public class GraphQlDelegatingHandler : DelegatingHandler - { - //private readonly ISchema _schema; - private readonly IDocumentExecuter _executer; - private readonly IDocumentWriter _writer; - - public GraphQlDelegatingHandler(IDocumentExecuter executer, IDocumentWriter writer) - { - _executer = executer; - _writer = writer; - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - //try get query from body, could check http method :) - var query = await request.Content.ReadAsStringAsync(cancellationToken); - - //if not body try query string, dont hack like this in real world.. - if (query.Length == 0) - { - var decoded = WebUtility.UrlDecode(request.RequestUri.Query); - query = decoded.Replace("?query=", string.Empty); - } - - var result = await _executer.ExecuteAsync(_ => - { - _.Query = query; - }); - - var responseBody = await _writer.WriteToStringAsync(result); - - //maybe check for errors and headers etc in real world? - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(responseBody) - }; - - //ocelot will treat this like any other http request... - return response; - } - } - - public class Program - { - public static void Main() - { - var schema = Schema.For(@" - type Hero { - id: Int - name: String - } - - type Query { - hero(id: Int): Hero - } - ", _ => - { - _.Types.Include(); - }); - - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json", false, false) - .AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(schema); - s.AddOcelot() - .AddDelegatingHandler(); - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - logging.AddConsole(); - }) - .UseIISIntegration() - .Configure(app => - { - app.UseOcelot().Wait(); - }) - .Build() - .Run(); - } - } -} +using GraphQL; +using GraphQL.Types; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace OcelotGraphQL +{ + public class Hero + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class Query + { + private readonly List _heroes = new() + { + new Hero { Id = 1, Name = "R2-D2" }, + new Hero { Id = 2, Name = "Batman" }, + new Hero { Id = 3, Name = "Wonder Woman" }, + new Hero { Id = 4, Name = "Tom Pallister" } + }; + + [GraphQLMetadata("hero")] + public Hero GetHero(int id) + { + return _heroes.FirstOrDefault(x => x.Id == id); + } + } + + public class GraphQlDelegatingHandler : DelegatingHandler + { + //private readonly ISchema _schema; + private readonly IDocumentExecuter _executer; + private readonly IDocumentWriter _writer; + + public GraphQlDelegatingHandler(IDocumentExecuter executer, IDocumentWriter writer) + { + _executer = executer; + _writer = writer; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + //try get query from body, could check http method :) + var query = await request.Content.ReadAsStringAsync(cancellationToken); + + //if not body try query string, dont hack like this in real world.. + if (query.Length == 0) + { + var decoded = WebUtility.UrlDecode(request.RequestUri.Query); + query = decoded.Replace("?query=", string.Empty); + } + + var result = await _executer.ExecuteAsync(_ => + { + _.Query = query; + }); + + var responseBody = await _writer.WriteToStringAsync(result); + + //maybe check for errors and headers etc in real world? + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseBody) + }; + + //ocelot will treat this like any other http request... + return response; + } + } + + public class Program + { + public static void Main() + { + var schema = Schema.For(@" + type Hero { + id: Int + name: String + } + + type Query { + hero(id: Int): Hero + } + ", _ => + { + _.Types.Include(); + }); + + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(schema); + s.AddOcelot() + .AddDelegatingHandler(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .UseIISIntegration() + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .Build() + .Run(); + } + } +} diff --git a/samples/OcelotGraphQL/Properties/launchSettings.json b/samples/GraphQL/Properties/launchSettings.json similarity index 100% rename from samples/OcelotGraphQL/Properties/launchSettings.json rename to samples/GraphQL/Properties/launchSettings.json diff --git a/samples/OcelotGraphQL/README.md b/samples/GraphQL/README.md similarity index 96% rename from samples/OcelotGraphQL/README.md rename to samples/GraphQL/README.md index 7f16ed985..1a470c344 100644 --- a/samples/OcelotGraphQL/README.md +++ b/samples/GraphQL/README.md @@ -1,71 +1,71 @@ -# Ocelot using GraphQL example - -Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. -I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorization / authentication or I would -bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. - -## Example - -If you run this project with - -$ dotnet run - -Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... - -GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } - -RESPONSE -```json - { - "data": { - "hero": { - "id": 4, - "name": "Tom Pallister" - } - } - } -``` - -POST http://localhost:5000/graphql - -BODY -```json - { hero(id: 4) { id name } } -``` - -RESPONSE -```json - { - "data": { - "hero": { - "id": 4, - "name": "Tom Pallister" - } - } - } -``` - -## Notes - -Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. - -```json -{ - "Routes": [ - { - "DownstreamPathTemplate": "/graphql", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "yourgraphqlhost.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/graphql", - "DelegatingHandlers": [ - "GraphQlDelegatingHandler" - ] - } - ] - } +# Ocelot using GraphQL example + +Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. +I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorization / authentication or I would +bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. + +## Example + +If you run this project with + +$ dotnet run + +Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... + +GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +POST http://localhost:5000/graphql + +BODY +```json + { hero(id: 4) { id name } } +``` + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +## Notes + +Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. + +```json +{ + "Routes": [ + { + "DownstreamPathTemplate": "/graphql", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "yourgraphqlhost.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } ``` \ No newline at end of file diff --git a/samples/OcelotGraphQL/ocelot.json b/samples/GraphQL/ocelot.json similarity index 96% rename from samples/OcelotGraphQL/ocelot.json rename to samples/GraphQL/ocelot.json index c716bf258..3529e0ba8 100644 --- a/samples/OcelotGraphQL/ocelot.json +++ b/samples/GraphQL/ocelot.json @@ -1,19 +1,19 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/graphql", - "DelegatingHandlers": [ - "GraphQlDelegatingHandler" - ] - } - ] - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } \ No newline at end of file diff --git a/samples/OcelotKube/.dockerignore b/samples/Kubernetes/.dockerignore similarity index 100% rename from samples/OcelotKube/.dockerignore rename to samples/Kubernetes/.dockerignore diff --git a/samples/OcelotKube/ApiGateway/Dockerfile b/samples/Kubernetes/ApiGateway/Dockerfile similarity index 100% rename from samples/OcelotKube/ApiGateway/Dockerfile rename to samples/Kubernetes/ApiGateway/Dockerfile diff --git a/samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj b/samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj similarity index 100% rename from samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj rename to samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj diff --git a/samples/OcelotKube/ApiGateway/Program.cs b/samples/Kubernetes/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotKube/ApiGateway/Program.cs rename to samples/Kubernetes/ApiGateway/Program.cs diff --git a/samples/OcelotKube/ApiGateway/Properties/launchSettings.json b/samples/Kubernetes/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotKube/ApiGateway/Properties/launchSettings.json rename to samples/Kubernetes/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotKube/ApiGateway/Startup.cs b/samples/Kubernetes/ApiGateway/Startup.cs similarity index 100% rename from samples/OcelotKube/ApiGateway/Startup.cs rename to samples/Kubernetes/ApiGateway/Startup.cs diff --git a/samples/OcelotKube/ApiGateway/appsettings.Development.json b/samples/Kubernetes/ApiGateway/appsettings.Development.json similarity index 100% rename from samples/OcelotKube/ApiGateway/appsettings.Development.json rename to samples/Kubernetes/ApiGateway/appsettings.Development.json diff --git a/samples/OcelotKube/DownstreamService/appsettings.json b/samples/Kubernetes/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotKube/DownstreamService/appsettings.json rename to samples/Kubernetes/ApiGateway/appsettings.json diff --git a/samples/OcelotKube/ApiGateway/ocelot.json b/samples/Kubernetes/ApiGateway/ocelot.json similarity index 95% rename from samples/OcelotKube/ApiGateway/ocelot.json rename to samples/Kubernetes/ApiGateway/ocelot.json index 6a28b9eec..f4a5af0b8 100644 --- a/samples/OcelotKube/ApiGateway/ocelot.json +++ b/samples/Kubernetes/ApiGateway/ocelot.json @@ -1,20 +1,20 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/api/values", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/values", - "ServiceName": "downstreamservice", - "UpstreamHttpMethod": [ "Get" ] - } - ], - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "192.168.0.13", - "Port": 443, - "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", - "Namespace": "dev", - "Type": "kube" - } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/values", + "ServiceName": "downstreamservice", + "UpstreamHttpMethod": [ "Get" ] + } + ], + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "192.168.0.13", + "Port": 443, + "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", + "Namespace": "dev", + "Type": "kube" + } + } +} diff --git a/samples/OcelotKube/Dockerfile b/samples/Kubernetes/Dockerfile similarity index 100% rename from samples/OcelotKube/Dockerfile rename to samples/Kubernetes/Dockerfile diff --git a/samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs b/samples/Kubernetes/DownstreamService/Controllers/ValuesController.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs rename to samples/Kubernetes/DownstreamService/Controllers/ValuesController.cs diff --git a/samples/OcelotKube/DownstreamService/Controllers/WeatherForecastController.cs b/samples/Kubernetes/DownstreamService/Controllers/WeatherForecastController.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Controllers/WeatherForecastController.cs rename to samples/Kubernetes/DownstreamService/Controllers/WeatherForecastController.cs diff --git a/samples/OcelotKube/DownstreamService/Dockerfile b/samples/Kubernetes/DownstreamService/Dockerfile similarity index 100% rename from samples/OcelotKube/DownstreamService/Dockerfile rename to samples/Kubernetes/DownstreamService/Dockerfile diff --git a/samples/OcelotKube/DownstreamService/Models/WeatherForecast.cs b/samples/Kubernetes/DownstreamService/Models/WeatherForecast.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Models/WeatherForecast.cs rename to samples/Kubernetes/DownstreamService/Models/WeatherForecast.cs diff --git a/samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj b/samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj similarity index 86% rename from samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj rename to samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj index 3c1cfefb2..72b36b2fc 100644 --- a/samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj +++ b/samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj @@ -1,6 +1,6 @@ - net7.0 + net6.0;net7.0;net8.0 disable disable InProcess diff --git a/samples/OcelotKube/DownstreamService/Program.cs b/samples/Kubernetes/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Program.cs rename to samples/Kubernetes/DownstreamService/Program.cs diff --git a/samples/OcelotKube/DownstreamService/Properties/launchSettings.json b/samples/Kubernetes/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotKube/DownstreamService/Properties/launchSettings.json rename to samples/Kubernetes/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotKube/DownstreamService/appsettings.Development.json b/samples/Kubernetes/DownstreamService/appsettings.Development.json similarity index 100% rename from samples/OcelotKube/DownstreamService/appsettings.Development.json rename to samples/Kubernetes/DownstreamService/appsettings.Development.json diff --git a/samples/OcelotBasic/appsettings.json b/samples/Kubernetes/DownstreamService/appsettings.json similarity index 92% rename from samples/OcelotBasic/appsettings.json rename to samples/Kubernetes/DownstreamService/appsettings.json index 7376aada1..def9159a7 100644 --- a/samples/OcelotBasic/appsettings.json +++ b/samples/Kubernetes/DownstreamService/appsettings.json @@ -1,8 +1,8 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/OcelotKube/OcelotKube.sln b/samples/Kubernetes/OcelotKube.sln similarity index 100% rename from samples/OcelotKube/OcelotKube.sln rename to samples/Kubernetes/OcelotKube.sln diff --git a/samples/Ocelot.Samples.sln b/samples/Ocelot.Samples.sln new file mode 100644 index 000000000..ca208f4a6 --- /dev/null +++ b/samples/Ocelot.Samples.sln @@ -0,0 +1,91 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.AdministrationApi", "Administration\Ocelot.Samples.AdministrationApi.csproj", "{238467FE-19EE-4102-9AF7-51EB2C6F0354}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Basic.ApiGateway", "Basic\Ocelot.Samples.Basic.ApiGateway.csproj", "{A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.ApiGateway", "Eureka\ApiGateway\Ocelot.Samples.Eureka.ApiGateway.csproj", "{EA0E146F-2C2B-4176-B6EC-F62A587F5077}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.DownstreamService", "Eureka\DownstreamService\Ocelot.Samples.Eureka.DownstreamService.csproj", "{B7317B64-2208-472D-90AC-F42B61956B79}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.GraphQL", "GraphQL\Ocelot.Samples.GraphQL.csproj", "{6CCA3677-420A-4294-8D41-67CF3D818575}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.ApiGateway", "Kubernetes\ApiGateway\Ocelot.Samples.Kubernetes.ApiGateway.csproj", "{721C1737-70CB-4B11-A19B-C7AAC6856CC7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.DownstreamService", "Kubernetes\DownstreamService\Ocelot.Samples.Kubernetes.DownstreamService.csproj", "{CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OpenTracing", "OpenTracing\Ocelot.Samples.OpenTracing.csproj", "{707BD584-3CC0-4087-820C-049C3D68F6A3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "ServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{96B9F16E-C95D-425A-A419-40CB3C90CB77}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "ServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{60E14B1A-C295-453B-910E-58E09F5A28AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.ApiGateway", "ServiceFabric\ApiGateway\Ocelot.Samples.ServiceFabric.ApiGateway.csproj", "{115F7934-3326-492A-B131-64F0EAEBAD71}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.DownstreamService", "ServiceFabric\DownstreamService\Ocelot.Samples.ServiceFabric.DownstreamService.csproj", "{6C777A20-F557-45CF-B87B-11E3C6B29A36}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Debug|Any CPU.Build.0 = Debug|Any CPU + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Release|Any CPU.ActiveCfg = Release|Any CPU + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Release|Any CPU.Build.0 = Release|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Release|Any CPU.Build.0 = Release|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.Build.0 = Release|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.Build.0 = Release|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.Build.0 = Release|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.Build.0 = Release|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.Build.0 = Release|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Release|Any CPU.Build.0 = Release|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.Build.0 = Release|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.Build.0 = Release|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.Build.0 = Release|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2C1620D4-EB38-4C3E-9FC5-029FB6B2F426} + EndGlobalSection +EndGlobal diff --git a/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore b/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore deleted file mode 100644 index e7b690f11..000000000 --- a/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore +++ /dev/null @@ -1,25 +0,0 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile b/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile deleted file mode 100644 index b7535cfcd..000000000 --- a/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base -WORKDIR /app -EXPOSE 80 -EXPOSE 443 - -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build -WORKDIR /src -COPY ["Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "."] -RUN dotnet restore "./Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -COPY . . -WORKDIR "/src/." -RUN dotnet build "Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Ocelot.Samples.ServiceDiscovery.DownstreamService.dll"] diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj b/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj deleted file mode 100644 index c0669cb7d..000000000 --- a/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net7.0 - disable - enable - Linux - . - d5492aa8-b50c-41ae-a044-9954846db9ac - - - - - - - - diff --git a/samples/OcelotOpenTracing/OcelotOpenTracing.csproj b/samples/OpenTracing/Ocelot.Samples.OpenTracing.csproj similarity index 100% rename from samples/OcelotOpenTracing/OcelotOpenTracing.csproj rename to samples/OpenTracing/Ocelot.Samples.OpenTracing.csproj diff --git a/samples/OcelotOpenTracing/Program.cs b/samples/OpenTracing/Program.cs similarity index 100% rename from samples/OcelotOpenTracing/Program.cs rename to samples/OpenTracing/Program.cs diff --git a/samples/OcelotOpenTracing/appsettings.Development.json b/samples/OpenTracing/appsettings.Development.json similarity index 100% rename from samples/OcelotOpenTracing/appsettings.Development.json rename to samples/OpenTracing/appsettings.Development.json diff --git a/samples/OcelotOpenTracing/appsettings.json b/samples/OpenTracing/appsettings.json similarity index 100% rename from samples/OcelotOpenTracing/appsettings.json rename to samples/OpenTracing/appsettings.json diff --git a/samples/OcelotOpenTracing/ocelot.json b/samples/OpenTracing/ocelot.json similarity index 100% rename from samples/OcelotOpenTracing/ocelot.json rename to samples/OpenTracing/ocelot.json diff --git a/samples/OcelotServiceDiscovery/.dockerignore b/samples/ServiceDiscovery/.dockerignore similarity index 100% rename from samples/OcelotServiceDiscovery/.dockerignore rename to samples/ServiceDiscovery/.dockerignore diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj b/samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj similarity index 59% rename from samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj rename to samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj index aac1dd20a..815c793e9 100644 --- a/samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj +++ b/samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj @@ -1,6 +1,8 @@ - net6.0;net7.0;net8.0 + net8.0 + disable + disable diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Program.cs b/samples/ServiceDiscovery/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/Program.cs rename to samples/ServiceDiscovery/ApiGateway/Program.cs diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json b/samples/ServiceDiscovery/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json rename to samples/ServiceDiscovery/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs b/samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs rename to samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs b/samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs rename to samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs diff --git a/samples/OcelotServiceDiscovery/ApiGateway/appsettings.json b/samples/ServiceDiscovery/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/appsettings.json rename to samples/ServiceDiscovery/ApiGateway/appsettings.json diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ocelot.json b/samples/ServiceDiscovery/ApiGateway/ocelot.json similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/ocelot.json rename to samples/ServiceDiscovery/ApiGateway/ocelot.json diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs b/samples/ServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs rename to samples/ServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs b/samples/ServiceDiscovery/DownstreamService/Controllers/HealthController.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs rename to samples/ServiceDiscovery/DownstreamService/Controllers/HealthController.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs b/samples/ServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs rename to samples/ServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs b/samples/ServiceDiscovery/DownstreamService/Models/HealthResult.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs rename to samples/ServiceDiscovery/DownstreamService/Models/HealthResult.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs b/samples/ServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs rename to samples/ServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs b/samples/ServiceDiscovery/DownstreamService/Models/ReadyResult.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs rename to samples/ServiceDiscovery/DownstreamService/Models/ReadyResult.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs b/samples/ServiceDiscovery/DownstreamService/Models/WeatherForecast.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs rename to samples/ServiceDiscovery/DownstreamService/Models/WeatherForecast.cs diff --git a/samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj b/samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj new file mode 100644 index 000000000..163a956bb --- /dev/null +++ b/samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + disable + enable + + + + + + + diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Program.cs b/samples/ServiceDiscovery/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Program.cs rename to samples/ServiceDiscovery/DownstreamService/Program.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json b/samples/ServiceDiscovery/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json rename to samples/ServiceDiscovery/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Startup.cs b/samples/ServiceDiscovery/DownstreamService/Startup.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Startup.cs rename to samples/ServiceDiscovery/DownstreamService/Startup.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json b/samples/ServiceDiscovery/DownstreamService/appsettings.Development.json similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json rename to samples/ServiceDiscovery/DownstreamService/appsettings.Development.json diff --git a/samples/OcelotServiceDiscovery/DownstreamService/appsettings.json b/samples/ServiceDiscovery/DownstreamService/appsettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/appsettings.json rename to samples/ServiceDiscovery/DownstreamService/appsettings.json diff --git a/samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln b/samples/ServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln similarity index 100% rename from samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln rename to samples/ServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln diff --git a/samples/OcelotServiceDiscovery/README.md b/samples/ServiceDiscovery/README.md similarity index 100% rename from samples/OcelotServiceDiscovery/README.md rename to samples/ServiceDiscovery/README.md diff --git a/samples/OcelotServiceFabric/.gitignore b/samples/ServiceFabric/.gitignore similarity index 100% rename from samples/OcelotServiceFabric/.gitignore rename to samples/ServiceFabric/.gitignore diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj b/samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj similarity index 92% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj rename to samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj index c97238c53..c9f886f75 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj +++ b/samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj @@ -18,6 +18,6 @@ - + diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs b/samples/ServiceFabric/ApiGateway/OcelotApplicationApiGateway.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs rename to samples/ServiceFabric/ApiGateway/OcelotApplicationApiGateway.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs b/samples/ServiceFabric/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs rename to samples/ServiceFabric/ApiGateway/Program.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Properties/launchSettings.json b/samples/ServiceFabric/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Properties/launchSettings.json rename to samples/ServiceFabric/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs b/samples/ServiceFabric/ApiGateway/ServiceEventListener.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs rename to samples/ServiceFabric/ApiGateway/ServiceEventListener.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs b/samples/ServiceFabric/ApiGateway/ServiceEventSource.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs rename to samples/ServiceFabric/ApiGateway/ServiceEventSource.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs b/samples/ServiceFabric/ApiGateway/WebCommunicationListener.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs rename to samples/ServiceFabric/ApiGateway/WebCommunicationListener.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/appsettings.json b/samples/ServiceFabric/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/appsettings.json rename to samples/ServiceFabric/ApiGateway/appsettings.json diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json b/samples/ServiceFabric/ApiGateway/ocelot.json similarity index 95% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json rename to samples/ServiceFabric/ApiGateway/ocelot.json index b541e95c4..1b174cd62 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json +++ b/samples/ServiceFabric/ApiGateway/ocelot.json @@ -1,21 +1,21 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/api/values", - "UpstreamPathTemplate": "/EquipmentInterfaces", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "ServiceName": "OcelotServiceApplication/OcelotApplicationService" - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 19081, - "Type": "ServiceFabric" - } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "UpstreamPathTemplate": "/EquipmentInterfaces", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "ServiceName": "OcelotServiceApplication/OcelotApplicationService" + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 19081, + "Type": "ServiceFabric" + } + } +} diff --git a/samples/OcelotServiceFabric/CONTRIBUTING.md b/samples/ServiceFabric/CONTRIBUTING.md similarity index 100% rename from samples/OcelotServiceFabric/CONTRIBUTING.md rename to samples/ServiceFabric/CONTRIBUTING.md diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs b/samples/ServiceFabric/DownstreamService/ApiGateway.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs rename to samples/ServiceFabric/DownstreamService/ApiGateway.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs b/samples/ServiceFabric/DownstreamService/Controllers/ValuesController.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs rename to samples/ServiceFabric/DownstreamService/Controllers/ValuesController.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj b/samples/ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj rename to samples/ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs b/samples/ServiceFabric/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs rename to samples/ServiceFabric/DownstreamService/Program.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Properties/launchSettings.json b/samples/ServiceFabric/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Properties/launchSettings.json rename to samples/ServiceFabric/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs b/samples/ServiceFabric/DownstreamService/ServiceEventSource.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs rename to samples/ServiceFabric/DownstreamService/ServiceEventSource.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs b/samples/ServiceFabric/DownstreamService/Startup.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs rename to samples/ServiceFabric/DownstreamService/Startup.cs diff --git a/samples/OcelotServiceFabric/LICENSE.md b/samples/ServiceFabric/LICENSE.md similarity index 100% rename from samples/OcelotServiceFabric/LICENSE.md rename to samples/ServiceFabric/LICENSE.md diff --git a/samples/OcelotServiceFabric/OcelotApplication/ApplicationManifest.xml b/samples/ServiceFabric/OcelotApplication/ApplicationManifest.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/ApplicationManifest.xml rename to samples/ServiceFabric/OcelotApplication/ApplicationManifest.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml diff --git a/samples/OcelotServiceFabric/README.md b/samples/ServiceFabric/README.md similarity index 100% rename from samples/OcelotServiceFabric/README.md rename to samples/ServiceFabric/README.md diff --git a/samples/OcelotServiceFabric/build.bat b/samples/ServiceFabric/build.bat similarity index 100% rename from samples/OcelotServiceFabric/build.bat rename to samples/ServiceFabric/build.bat diff --git a/samples/OcelotServiceFabric/build.sh b/samples/ServiceFabric/build.sh similarity index 100% rename from samples/OcelotServiceFabric/build.sh rename to samples/ServiceFabric/build.sh diff --git a/samples/OcelotServiceFabric/dotnet-include.sh b/samples/ServiceFabric/dotnet-include.sh similarity index 100% rename from samples/OcelotServiceFabric/dotnet-include.sh rename to samples/ServiceFabric/dotnet-include.sh diff --git a/samples/OcelotServiceFabric/install.ps1 b/samples/ServiceFabric/install.ps1 similarity index 100% rename from samples/OcelotServiceFabric/install.ps1 rename to samples/ServiceFabric/install.ps1 diff --git a/samples/OcelotServiceFabric/install.sh b/samples/ServiceFabric/install.sh similarity index 100% rename from samples/OcelotServiceFabric/install.sh rename to samples/ServiceFabric/install.sh diff --git a/samples/OcelotServiceFabric/uninstall.ps1 b/samples/ServiceFabric/uninstall.ps1 similarity index 100% rename from samples/OcelotServiceFabric/uninstall.ps1 rename to samples/ServiceFabric/uninstall.ps1 diff --git a/samples/OcelotServiceFabric/uninstall.sh b/samples/ServiceFabric/uninstall.sh similarity index 100% rename from samples/OcelotServiceFabric/uninstall.sh rename to samples/ServiceFabric/uninstall.sh diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index 273fca2ab..27b5b4422 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -1,5 +1,5 @@ -using Ocelot.Infrastructure.Extensions; -using Ocelot.Logging; +using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; @@ -7,70 +7,49 @@ namespace Ocelot.Provider.Consul; public class Consul : IServiceDiscoveryProvider { - private const string VersionPrefix = "version-"; - private readonly ConsulRegistryConfiguration _config; + private readonly ConsulRegistryConfiguration _configuration; private readonly IConsulClient _consul; private readonly IOcelotLogger _logger; + private readonly IConsulServiceBuilder _serviceBuilder; - public Consul(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory) + public Consul( + ConsulRegistryConfiguration config, + IOcelotLoggerFactory factory, + IConsulClientFactory clientFactory, + IConsulServiceBuilder serviceBuilder) { - _config = config; - _consul = clientFactory.Get(_config); + _configuration = config; + _consul = clientFactory.Get(_configuration); _logger = factory.CreateLogger(); + _serviceBuilder = serviceBuilder; } - public async Task> GetAsync() + public virtual async Task> GetAsync() { - var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); + var entriesTask = _consul.Health.Service(_configuration.KeyOfServiceInConsul, string.Empty, true); + var nodesTask = _consul.Catalog.Nodes(); + await Task.WhenAll(entriesTask, nodesTask); + + var entries = entriesTask.Result.Response ?? Array.Empty(); + var nodes = nodesTask.Result.Response ?? Array.Empty(); var services = new List(); - foreach (var serviceEntry in queryResult.Response) + if (entries.Length != 0) { - var service = serviceEntry.Service; - if (IsValid(service)) - { - var nodes = await _consul.Catalog.Nodes(); - if (nodes.Response == null) - { - services.Add(BuildService(serviceEntry, null)); - } - else - { - var serviceNode = nodes.Response.FirstOrDefault(n => n.Address == service.Address); - services.Add(BuildService(serviceEntry, serviceNode)); - } - } - else - { - _logger.LogWarning( - () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); - } + _logger.LogDebug(() => $"{nameof(Consul)} Provider: Found total {entries.Length} service entries for '{_configuration.KeyOfServiceInConsul}' service."); + _logger.LogDebug(() => $"{nameof(Consul)} Provider: Found total {nodes.Length} catalog nodes."); + var collection = BuildServices(entries, nodes); + services.AddRange(collection); + } + else + { + _logger.LogWarning(() => $"{nameof(Consul)} Provider: No service entries found for '{_configuration.KeyOfServiceInConsul}' service!"); } - return services.ToList(); - } - - private static Service BuildService(ServiceEntry serviceEntry, Node serviceNode) - { - var service = serviceEntry.Service; - return new Service( - service.Service, - new ServiceHostAndPort( - serviceNode == null ? service.Address : serviceNode.Name, - service.Port), - service.ID, - GetVersionFromStrings(service.Tags), - service.Tags ?? Enumerable.Empty()); + return services; } - private static bool IsValid(AgentService service) - => !string.IsNullOrEmpty(service.Address) - && !service.Address.Contains($"{Uri.UriSchemeHttp}://") - && !service.Address.Contains($"{Uri.UriSchemeHttps}://") - && service.Port > 0; - - private static string GetVersionFromStrings(IEnumerable strings) - => strings?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) - .TrimStart(VersionPrefix); + protected virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) + => _serviceBuilder.BuildServices(entries, nodes); } diff --git a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs index 4a7478c59..f7c5c0c0c 100644 --- a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs @@ -1,4 +1,6 @@ -namespace Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; + +namespace Ocelot.Provider.Consul; public class ConsulClientFactory : IConsulClientFactory { diff --git a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs index 2f9569362..c95146f46 100644 --- a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs +++ b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs @@ -5,6 +5,7 @@ using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.Responses; using System.Text; diff --git a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs index c769f49c0..00c2715ee 100644 --- a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.Provider.Consul; @@ -17,16 +18,20 @@ public static class ConsulProviderFactory public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider; + private static ConsulRegistryConfiguration configuration; + private static ConsulRegistryConfiguration ConfigurationGetter() => configuration; + public static Func GetConfiguration { get; } = ConfigurationGetter; + private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route) { var factory = provider.GetService(); var consulFactory = provider.GetService(); - var consulRegistryConfiguration = new ConsulRegistryConfiguration( - config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); + configuration = new ConsulRegistryConfiguration(config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); + var serviceBuilder = provider.GetService(); - var consulProvider = new Consul(consulRegistryConfiguration, factory, consulFactory); + var consulProvider = new Consul(configuration, factory, consulFactory, serviceBuilder); if (PollConsul.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs new file mode 100644 index 000000000..7526bea65 --- /dev/null +++ b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs @@ -0,0 +1,103 @@ +using Ocelot.Infrastructure.Extensions; +using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; +using Ocelot.Values; + +namespace Ocelot.Provider.Consul; + +public class DefaultConsulServiceBuilder : IConsulServiceBuilder +{ + private readonly ConsulRegistryConfiguration _configuration; + private readonly IConsulClient _client; + private readonly IOcelotLogger _logger; + + public DefaultConsulServiceBuilder( + Func configurationFactory, + IConsulClientFactory clientFactory, + IOcelotLoggerFactory loggerFactory) + { + _configuration = configurationFactory.Invoke(); + _client = clientFactory.Get(_configuration); + _logger = loggerFactory.CreateLogger(); + } + + public ConsulRegistryConfiguration Configuration => _configuration; + protected IConsulClient Client => _client; + protected IOcelotLogger Logger => _logger; + + public virtual bool IsValid(ServiceEntry entry) + { + var service = entry.Service; + var address = service.Address; + bool valid = !string.IsNullOrEmpty(address) + && !address.StartsWith(Uri.UriSchemeHttp + "://", StringComparison.OrdinalIgnoreCase) + && !address.StartsWith(Uri.UriSchemeHttps + "://", StringComparison.OrdinalIgnoreCase) + && service.Port > 0; + + if (!valid) + { + _logger.LogWarning( + () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); + } + + return valid; + } + + public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) + { + ArgumentNullException.ThrowIfNull(entries); + var services = new List(entries.Length); + + foreach (var serviceEntry in entries) + { + if (IsValid(serviceEntry)) + { + var serviceNode = GetNode(serviceEntry, nodes); + var item = CreateService(serviceEntry, serviceNode); + if (item != null) + { + services.Add(item); + } + } + } + + return services; + } + + protected virtual Node GetNode(ServiceEntry entry, Node[] nodes) + => entry?.Node ?? nodes?.FirstOrDefault(n => n.Address == entry?.Service?.Address); + + public virtual Service CreateService(ServiceEntry entry, Node node) + => new( + GetServiceName(entry, node), + GetServiceHostAndPort(entry, node), + GetServiceId(entry, node), + GetServiceVersion(entry, node), + GetServiceTags(entry, node) + ); + + protected virtual string GetServiceName(ServiceEntry entry, Node node) + => entry.Service.Service; + + protected virtual ServiceHostAndPort GetServiceHostAndPort(ServiceEntry entry, Node node) + => new( + GetDownstreamHost(entry, node), + entry.Service.Port); + + protected virtual string GetDownstreamHost(ServiceEntry entry, Node node) + => node != null ? node.Name : entry.Service.Address; + + protected virtual string GetServiceId(ServiceEntry entry, Node node) + => entry.Service.ID; + + protected virtual string GetServiceVersion(ServiceEntry entry, Node node) + => entry.Service.Tags + ?.FirstOrDefault(tag => tag.StartsWith(VersionPrefix, StringComparison.Ordinal)) + ?.TrimStart(VersionPrefix) + ?? string.Empty; + + protected virtual IEnumerable GetServiceTags(ServiceEntry entry, Node node) + => entry.Service.Tags ?? Enumerable.Empty(); + + private const string VersionPrefix = "version-"; +} diff --git a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs b/src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs similarity index 68% rename from src/Ocelot.Provider.Consul/IConsulClientFactory.cs rename to src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs index 3ee3a2b25..0fe12aa08 100644 --- a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs @@ -1,4 +1,4 @@ -namespace Ocelot.Provider.Consul; +namespace Ocelot.Provider.Consul.Interfaces; public interface IConsulClientFactory { diff --git a/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs new file mode 100644 index 000000000..0555b0144 --- /dev/null +++ b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs @@ -0,0 +1,11 @@ +using Ocelot.Values; + +namespace Ocelot.Provider.Consul.Interfaces; + +public interface IConsulServiceBuilder +{ + ConsulRegistryConfiguration Configuration { get; } + bool IsValid(ServiceEntry entry); + IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes); + Service CreateService(ServiceEntry serviceEntry, Node serviceNode); +} diff --git a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs index dac7aecff..0c064f780 100644 --- a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs @@ -2,21 +2,57 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; +using Ocelot.Provider.Consul.Interfaces; namespace Ocelot.Provider.Consul; public static class OcelotBuilderExtensions { + /// + /// Integrates Consul service discovery into the DI, atop the existing Ocelot services. + /// + /// + /// Default services: + /// + /// The service is an instance of . + /// The service is an instance of . + /// + /// + /// The Ocelot Builder instance, default. + /// The reference to the same extended object. public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) { builder.Services .AddSingleton(ConsulProviderFactory.Get) + .AddSingleton(ConsulProviderFactory.GetConfiguration) .AddSingleton() + .AddSingleton() .RemoveAll(typeof(IFileConfigurationPollerOptions)) .AddSingleton(); return builder; } + /// + /// Integrates Consul service discovery into the DI, atop the existing Ocelot services, with service builder overriding. + /// + /// + /// Services to override: + /// + /// The service has been substituted with a instance. + /// + /// + /// The service builder type. + /// The Ocelot Builder instance, default. + /// The reference to the same extended object. + public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) + where TServiceBuilder : class, IConsulServiceBuilder + { + AddConsul(builder).Services + .RemoveAll() + .AddSingleton(typeof(IConsulServiceBuilder), typeof(TServiceBuilder)); + return builder; + } + public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder) { builder.Services diff --git a/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs b/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs index 22f58e538..83418957b 100644 --- a/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs +++ b/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs @@ -1,6 +1,7 @@ using HTTPlease; using KubeClient.Models; using KubeClient.ResourceClients; +using Ocelot.Provider.Kubernetes.Interfaces; namespace Ocelot.Provider.Kubernetes { diff --git a/src/Ocelot.Provider.Kubernetes/IEndPointClient.cs b/src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs similarity index 83% rename from src/Ocelot.Provider.Kubernetes/IEndPointClient.cs rename to src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs index 6dfca972d..10f79f8af 100644 --- a/src/Ocelot.Provider.Kubernetes/IEndPointClient.cs +++ b/src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs @@ -1,7 +1,7 @@ using KubeClient.Models; using KubeClient.ResourceClients; -namespace Ocelot.Provider.Kubernetes; +namespace Ocelot.Provider.Kubernetes.Interfaces; public interface IEndPointClient : IKubeResourceClient { diff --git a/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs new file mode 100644 index 000000000..d5c6bcc30 --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs @@ -0,0 +1,9 @@ +using KubeClient.Models; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes.Interfaces; + +public interface IKubeServiceBuilder +{ + IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint); +} diff --git a/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs new file mode 100644 index 000000000..a6ace7b2d --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs @@ -0,0 +1,10 @@ +using KubeClient.Models; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes.Interfaces; + +public interface IKubeServiceCreator +{ + IEnumerable Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset); + IEnumerable CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address); +} diff --git a/src/Ocelot.Provider.Kubernetes/Kube.cs b/src/Ocelot.Provider.Kubernetes/Kube.cs index 15b5cf6cc..5350f43b9 100644 --- a/src/Ocelot.Provider.Kubernetes/Kube.cs +++ b/src/Ocelot.Provider.Kubernetes/Kube.cs @@ -1,5 +1,6 @@ using KubeClient.Models; using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.Provider.Kubernetes; @@ -9,47 +10,44 @@ namespace Ocelot.Provider.Kubernetes; /// public class Kube : IServiceDiscoveryProvider { - private readonly KubeRegistryConfiguration _kubeRegistryConfiguration; + private readonly KubeRegistryConfiguration _configuration; private readonly IOcelotLogger _logger; private readonly IKubeApiClient _kubeApi; - - public Kube(KubeRegistryConfiguration kubeRegistryConfiguration, IOcelotLoggerFactory factory, IKubeApiClient kubeApi) + private readonly IKubeServiceBuilder _serviceBuilder; + private readonly List _services; + + public Kube( + KubeRegistryConfiguration configuration, + IOcelotLoggerFactory factory, + IKubeApiClient kubeApi, + IKubeServiceBuilder serviceBuilder) { - _kubeRegistryConfiguration = kubeRegistryConfiguration; + _configuration = configuration; _logger = factory.CreateLogger(); _kubeApi = kubeApi; + _serviceBuilder = serviceBuilder; + _services = new(); } - public async Task> GetAsync() + public virtual async Task> GetAsync() { var endpoint = await _kubeApi .ResourceClient(client => new EndPointClientV1(client)) - .GetAsync(_kubeRegistryConfiguration.KeyOfServiceInK8s, _kubeRegistryConfiguration.KubeNamespace); + .GetAsync(_configuration.KeyOfServiceInK8s, _configuration.KubeNamespace); - var services = new List(); - if (endpoint != null && endpoint.Subsets.Any()) + _services.Clear(); + if (endpoint?.Subsets.Count != 0) { - services.AddRange(BuildServices(endpoint)); + _services.AddRange(BuildServices(_configuration, endpoint)); } else { - _logger.LogWarning(() => $"namespace:{_kubeRegistryConfiguration.KubeNamespace}service:{_kubeRegistryConfiguration.KeyOfServiceInK8s} Unable to use ,it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); + _logger.LogWarning(() => $"K8s Namespace:{_configuration.KubeNamespace}, Service:{_configuration.KeyOfServiceInK8s}; Unable to use: it is invalid. Address must contain host only e.g. localhost and port must be greater than 0!"); } - return services; + return _services; } - private static List BuildServices(EndpointsV1 endpoint) - { - var services = new List(); - - foreach (var subset in endpoint.Subsets) - { - services.AddRange(subset.Addresses.Select(address => new Service(endpoint.Metadata.Name, - new ServiceHostAndPort(address.Ip, subset.Ports.First().Port), - endpoint.Metadata.Uid, string.Empty, Enumerable.Empty()))); - } - - return services; - } + protected virtual IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint) + => _serviceBuilder.BuildServices(configuration, endpoint); } diff --git a/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs b/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs index 2a3d7e815..b264e1b67 100644 --- a/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs +++ b/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs @@ -1,8 +1,8 @@ -namespace Ocelot.Provider.Kubernetes +namespace Ocelot.Provider.Kubernetes; + +public class KubeRegistryConfiguration { - public class KubeRegistryConfiguration - { - public string KubeNamespace { get; set; } - public string KeyOfServiceInK8s { get; set; } - } + public string KubeNamespace { get; set; } + public string KeyOfServiceInK8s { get; set; } + public string Scheme { get; set; } } diff --git a/src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs b/src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs new file mode 100644 index 000000000..589cfe5ba --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs @@ -0,0 +1,36 @@ +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes; + +public class KubeServiceBuilder : IKubeServiceBuilder +{ + private readonly IOcelotLogger _logger; + private readonly IKubeServiceCreator _serviceCreator; + + public KubeServiceBuilder(IOcelotLoggerFactory factory, IKubeServiceCreator serviceCreator) + { + ArgumentNullException.ThrowIfNull(factory); + _logger = factory.CreateLogger(); + + ArgumentNullException.ThrowIfNull(serviceCreator); + _serviceCreator = serviceCreator; + } + + public virtual IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint) + { + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(endpoint); + + var services = endpoint.Subsets + .SelectMany(subset => _serviceCreator.Create(configuration, endpoint, subset)) + .ToArray(); + + _logger.LogDebug(() => $"K8s '{Check(endpoint.Kind)}:{Check(endpoint.ApiVersion)}:{Check(endpoint.Metadata?.Name)}' endpoint: Total built {services.Length} services."); + return services; + } + + private static string Check(string str) => string.IsNullOrEmpty(str) ? "?" : str; +} diff --git a/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs b/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs new file mode 100644 index 000000000..3d51159c3 --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs @@ -0,0 +1,59 @@ +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes; + +public class KubeServiceCreator : IKubeServiceCreator +{ + private readonly IOcelotLogger _logger; + + public KubeServiceCreator(IOcelotLoggerFactory factory) + { + ArgumentNullException.ThrowIfNull(factory); + _logger = factory.CreateLogger(); + } + + public virtual IEnumerable Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset) + => (configuration == null || endpoint == null || subset == null) + ? Array.Empty() + : subset.Addresses + .SelectMany(address => CreateInstance(configuration, endpoint, subset, address)) + .ToArray(); + + public virtual IEnumerable CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + { + var instance = new Service( + GetServiceName(configuration, endpoint, subset, address), + GetServiceHostAndPort(configuration, endpoint, subset, address), + GetServiceId(configuration, endpoint, subset, address), + GetServiceVersion(configuration, endpoint, subset, address), + GetServiceTags(configuration, endpoint, subset, address) + ); + return new Service[] { instance }; + } + + protected virtual string GetServiceName(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => endpoint.Metadata?.Name; + + 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); + var portV1 = string.IsNullOrEmpty(configuration.Scheme) || !ports.Any(portNameToScheme) + ? ports.FirstOrDefault() + : ports.FirstOrDefault(portNameToScheme); + portV1 ??= new(); + portV1.Name ??= configuration.Scheme ?? string.Empty; + _logger.LogDebug(() => $"K8s service with key '{configuration.KeyOfServiceInK8s}' and address {address.Ip}; Detected port is {portV1.Name}:{portV1.Port}. Total {ports.Count} ports of [{string.Join(',', ports.Select(p => p.Name))}]."); + return new ServiceHostAndPort(address.Ip, portV1.Port, portV1.Name); + } + + protected virtual string GetServiceId(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => endpoint.Metadata?.Uid; + protected virtual string GetServiceVersion(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => endpoint.ApiVersion; + protected virtual IEnumerable GetServiceTags(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => Enumerable.Empty(); +} diff --git a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs index 4507c03e6..a3a1d48c0 100644 --- a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs +++ b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; -using Ocelot.Logging; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; namespace Ocelot.Provider.Kubernetes { @@ -17,14 +18,16 @@ private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provide { var factory = provider.GetService(); var kubeClient = provider.GetService(); + var serviceBuilder = provider.GetService(); var configuration = new KubeRegistryConfiguration { KeyOfServiceInK8s = route.ServiceName, KubeNamespace = string.IsNullOrEmpty(route.ServiceNamespace) ? config.Namespace : route.ServiceNamespace, + Scheme = route.DownstreamScheme, }; - var defaultK8sProvider = new Kube(configuration, factory, kubeClient); + var defaultK8sProvider = new Kube(configuration, factory, kubeClient, serviceBuilder); return PollKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase) ? new PollKube(config.PollingInterval, factory, defaultK8sProvider) diff --git a/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs index 0110bddbe..fadedc356 100644 --- a/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs @@ -1,16 +1,18 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; +using Ocelot.Provider.Kubernetes.Interfaces; -namespace Ocelot.Provider.Kubernetes +namespace Ocelot.Provider.Kubernetes; + +public static class OcelotBuilderExtensions { - public static class OcelotBuilderExtensions + public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true) { - public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true) - { - builder.Services - .AddSingleton(KubernetesProviderFactory.Get) - .AddKubeClient(usePodServiceAccount); - return builder; - } + builder.Services + .AddKubeClient(usePodServiceAccount) + .AddSingleton(KubernetesProviderFactory.Get) + .AddSingleton() + .AddSingleton(); + return builder; } } diff --git a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj index 176e4769f..0d9c84e67 100644 --- a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj +++ b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj @@ -34,7 +34,7 @@ all - + diff --git a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs index bfa26cf50..76fcc35d5 100644 --- a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs @@ -6,7 +6,6 @@ using Ocelot.Errors.QoS; using Ocelot.Logging; using Ocelot.Provider.Polly.Interfaces; -using Ocelot.Provider.Polly.v7; using Ocelot.Requester; using Polly.CircuitBreaker; using Polly.Registry; @@ -32,7 +31,7 @@ public static class OcelotBuilderExtensions /// /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. /// - /// QoS provider to use (by default use ). + /// QoS provider to use (by default use ). /// Ocelot builder to extend. /// Your customized delegating handler (to manage QoS behavior by yourself). /// Your customized error mapping. @@ -94,7 +93,7 @@ public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) /// Defaults: /// /// - /// + /// /// /// /// @@ -112,96 +111,4 @@ public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) /// A object, but concrete type is the class. private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) => new PollyResiliencePipelineDelegatingHandler(route, contextAccessor, loggerFactory); - - #region Obsolete extensions will be removed in future version - - /// - /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized delegating handler (to manage QoS behavior by yourself). - /// Your customized error mapping. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler, IDictionary> errorMapping) - where TProvider : class, IPollyQoSProvider - { - builder.Services - .AddSingleton(errorMapping) - .AddSingleton, TProvider>() - .AddSingleton(delegatingHandler); - return builder; - } - - /// - /// Adds Polly QoS provider to Ocelot with custom error mapping, but default is used. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized error mapping. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, IDictionary> errorMapping) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, GetDelegatingHandlerV7, errorMapping); - - /// - /// Adds Polly QoS provider to Ocelot with custom delegate, but default error mapping is used. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized delegating handler (to manage QoS behavior by yourself). - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, delegatingHandler, DefaultErrorMapping); - - /// - /// Adds Polly QoS provider to Ocelot by defaults. - /// - /// - /// Defaults: - /// - /// - /// - /// - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); - - /// - /// Adds Polly QoS provider to Ocelot by defaults with default QoS provider. - /// - /// - /// Defaults: - /// - /// - /// - /// - /// - /// - /// Ocelot builder to extend. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) - => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); - - /// - /// Creates default delegating handler based on the type. - /// - /// The downstream route to apply the handler for. - /// The context accessor of the route. - /// The factory of logger. - /// A object, but concrete type is the class. - private static DelegatingHandler GetDelegatingHandlerV7(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) - => new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory); - - #endregion } diff --git a/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs b/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs deleted file mode 100644 index 31ca23451..000000000 --- a/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Ocelot.Configuration; -using System.Net; - -namespace Ocelot.Provider.Polly; - -public abstract class PollyQoSProviderBase -{ - protected static readonly HashSet ServerErrorCodes = - [ - HttpStatusCode.InternalServerError, - HttpStatusCode.NotImplemented, - HttpStatusCode.BadGateway, - HttpStatusCode.ServiceUnavailable, - HttpStatusCode.GatewayTimeout, - HttpStatusCode.HttpVersionNotSupported, - HttpStatusCode.VariantAlsoNegotiates, - HttpStatusCode.InsufficientStorage, - HttpStatusCode.LoopDetected, - ]; - - protected static string GetRouteName(DownstreamRoute route) - => string.IsNullOrWhiteSpace(route.ServiceName) - ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty - : route.ServiceName; -} diff --git a/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs b/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs index be2df16b3..a0000c62d 100644 --- a/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs +++ b/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs @@ -4,24 +4,46 @@ using Polly.CircuitBreaker; using Polly.Registry; using Polly.Timeout; +using System.Net; namespace Ocelot.Provider.Polly; /// /// Default provider for Polly V8 pipelines. /// -public class PollyQoSResiliencePipelineProvider : PollyQoSProviderBase, IPollyQoSResiliencePipelineProvider +public class PollyQoSResiliencePipelineProvider : IPollyQoSResiliencePipelineProvider { - private readonly ResiliencePipelineRegistry _resiliencePipelineRegistry; + private readonly ResiliencePipelineRegistry _registry; private readonly IOcelotLogger _logger; - public PollyQoSResiliencePipelineProvider(IOcelotLoggerFactory loggerFactory, - ResiliencePipelineRegistry resiliencePipelineRegistry) + public PollyQoSResiliencePipelineProvider( + IOcelotLoggerFactory loggerFactory, + ResiliencePipelineRegistry registry) { - _resiliencePipelineRegistry = resiliencePipelineRegistry; _logger = loggerFactory.CreateLogger(); + _registry = registry; } + protected static readonly HashSet DefaultServerErrorCodes = new() + { + HttpStatusCode.InternalServerError, + HttpStatusCode.NotImplemented, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout, + HttpStatusCode.HttpVersionNotSupported, + HttpStatusCode.VariantAlsoNegotiates, + HttpStatusCode.InsufficientStorage, + HttpStatusCode.LoopDetected, + }; + + protected virtual HashSet ServerErrorCodes { get; } = DefaultServerErrorCodes; + + protected virtual string GetRouteName(DownstreamRoute route) + => string.IsNullOrWhiteSpace(route.ServiceName) + ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty + : route.ServiceName; + /// /// Gets Polly V8 resilience pipeline (applies QoS feature) for the route. /// @@ -29,56 +51,86 @@ public PollyQoSResiliencePipelineProvider(IOcelotLoggerFactory loggerFactory, /// A object where T is . public ResiliencePipeline GetResiliencePipeline(DownstreamRoute route) { - var options = route.QosOptions; - - // Check if we need pipeline at all before calling GetOrAddPipeline - if (options is null || - (options.ExceptionsAllowedBeforeBreaking == 0 && options.TimeoutValue is int.MaxValue)) + var options = route?.QosOptions; + if (options is null || !options.UseQos) { - return null; // shortcut > no qos + return ResiliencePipeline.Empty; // shortcut -> No QoS } var currentRouteName = GetRouteName(route); - return _resiliencePipelineRegistry.GetOrAddPipeline( - key: new OcelotResiliencePipelineKey(currentRouteName), - configure: (builder) => PollyResiliencePipelineWrapperFactory(builder, route)); + return _registry.GetOrAddPipeline( + key: new OcelotResiliencePipelineKey(currentRouteName), + configure: (builder) => ConfigureStrategies(builder, route)); } - private void PollyResiliencePipelineWrapperFactory(ResiliencePipelineBuilder builder, DownstreamRoute route) + protected virtual void ConfigureStrategies(ResiliencePipelineBuilder builder, DownstreamRoute route) { - var options = route.QosOptions; - - // Add TimeoutStrategy if TimeoutValue is not int.MaxValue and greater than 0 - if (options.TimeoutValue != int.MaxValue && options.TimeoutValue > 0) - { - builder.AddTimeout(TimeSpan.FromMilliseconds(options.TimeoutValue)); - } + ConfigureCircuitBreaker(builder, route); + ConfigureTimeout(builder, route); + } - // Add CircuitBreakerStrategy only if ExceptionsAllowedBeforeBreaking is greater than 0 - if (options.ExceptionsAllowedBeforeBreaking <= 0) + protected virtual ResiliencePipelineBuilder ConfigureCircuitBreaker(ResiliencePipelineBuilder builder, DownstreamRoute route) + { + // Add CircuitBreaker strategy only if ExceptionsAllowedBeforeBreaking is greater/equal than/to 2 + if (route.QosOptions.ExceptionsAllowedBeforeBreaking < 2) { - return; // shortcut > no qos (no timeout, no ExceptionsAllowedBeforeBreaking) + return builder; } + var options = route.QosOptions; var info = $"Circuit Breaker for Route: {GetRouteName(route)}: "; - - var circuitBreakerStrategyOptions = new CircuitBreakerStrategyOptions + var strategyOptions = new CircuitBreakerStrategyOptions { FailureRatio = 0.8, SamplingDuration = TimeSpan.FromSeconds(10), - MinimumThroughput = options.ExceptionsAllowedBeforeBreaking, - BreakDuration = TimeSpan.FromMilliseconds(options.DurationOfBreak), + MinimumThroughput = options.ExceptionsAllowedBeforeBreaking, + BreakDuration = options.DurationOfBreak > QoSOptions.LowBreakDuration + ? TimeSpan.FromMilliseconds(options.DurationOfBreak) + : TimeSpan.FromMilliseconds(QoSOptions.DefaultBreakDuration), ShouldHandle = new PredicateBuilder() .HandleResult(message => ServerErrorCodes.Contains(message.StatusCode)) .Handle() .Handle(), OnOpened = args => { - _logger.LogError(info + $"Breaking for {args.BreakDuration.TotalMilliseconds} ms", args.Outcome.Exception); + _logger.LogError(info + $"Breaking for {args.BreakDuration.TotalMilliseconds} ms", + args.Outcome.Exception); + return ValueTask.CompletedTask; + }, + OnClosed = _ => + { + _logger.LogInformation(info + "Closed"); + return ValueTask.CompletedTask; + }, + OnHalfOpened = _ => + { + _logger.LogInformation(info + "Half Opened"); return ValueTask.CompletedTask; }, }; + return builder.AddCircuitBreaker(strategyOptions); + } + + protected virtual ResiliencePipelineBuilder ConfigureTimeout(ResiliencePipelineBuilder builder, DownstreamRoute route) + { + var options = route.QosOptions; - builder.AddCircuitBreaker(circuitBreakerStrategyOptions); + // Add Timeout strategy if TimeoutValue is not int.MaxValue and greater than 0 + // TimeoutValue must be defined in QosOptions! + if (options.TimeoutValue == int.MaxValue || options.TimeoutValue <= 0) + { + return builder; + } + + var strategyOptions = new TimeoutStrategyOptions + { + Timeout = TimeSpan.FromMilliseconds(options.TimeoutValue), + OnTimeout = _ => + { + _logger.LogInformation($"Timeout for Route: {GetRouteName(route)}"); + return ValueTask.CompletedTask; + }, + }; + return builder.AddTimeout(strategyOptions); } } diff --git a/src/Ocelot.Provider.Polly/Usings.cs b/src/Ocelot.Provider.Polly/Usings.cs index fb5c12dc6..be2de9d09 100644 --- a/src/Ocelot.Provider.Polly/Usings.cs +++ b/src/Ocelot.Provider.Polly/Usings.cs @@ -1,10 +1,8 @@ // Default Microsoft.NET.Sdk namespaces +// Project extra global namespaces +global using Polly; global using System; global using System.Collections.Generic; -global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; - -// Project extra global namespaces -global using Polly; diff --git a/src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs b/src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs deleted file mode 100644 index 1537439b9..000000000 --- a/src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Ocelot.Configuration; - -namespace Ocelot.Provider.Polly.v7; - -[Obsolete("It is obsolete because now, we use IPollyQoSResiliencePipelineProvider with new v8 resilience strategies")] -public interface IPollyQoSProvider - where TResult : class -{ - PollyPolicyWrapper GetPollyPolicyWrapper(DownstreamRoute route); -} diff --git a/src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs deleted file mode 100644 index 6098bbe01..000000000 --- a/src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs +++ /dev/null @@ -1,120 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.DependencyInjection; -using Ocelot.Errors; -using Ocelot.Errors.QoS; -using Ocelot.Logging; -using Ocelot.Requester; -using Polly.CircuitBreaker; -using Polly.Timeout; - -namespace Ocelot.Provider.Polly.v7; - -public static class OcelotBuilderExtensions -{ - /// - /// Default mapping of Polly s to objects. - /// - public static readonly IDictionary> DefaultErrorMapping = new Dictionary> - { - {typeof(TaskCanceledException), CreateRequestTimedOutError}, - {typeof(TimeoutRejectedException), CreateRequestTimedOutError}, - {typeof(BrokenCircuitException), CreateRequestTimedOutError}, - {typeof(BrokenCircuitException), CreateRequestTimedOutError}, - }; - - private static Error CreateRequestTimedOutError(Exception e) => new RequestTimedOutError(e); - - #region Obsolete extensions will be removed in future version - - /// - /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized delegating handler (to manage QoS behavior by yourself). - /// Your customized error mapping. - /// The reference to the same extended object. - [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler, IDictionary> errorMapping) - where TProvider : class, IPollyQoSProvider - { - builder.Services - .AddSingleton(errorMapping) - .AddSingleton, TProvider>() - .AddSingleton(delegatingHandler); - return builder; - } - - /// - /// Adds Polly QoS provider to Ocelot with custom error mapping, but default is used. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized error mapping. - /// The reference to the same extended object. - [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, IDictionary> errorMapping) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, GetDelegatingHandlerV7, errorMapping); - - /// - /// Adds Polly QoS provider to Ocelot with custom delegate, but default error mapping is used. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized delegating handler (to manage QoS behavior by yourself). - /// The reference to the same extended object. - [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, delegatingHandler, DefaultErrorMapping); - - /// - /// Adds Polly QoS provider to Ocelot by defaults. - /// - /// - /// Defaults: - /// - /// - /// - /// - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); - - /// - /// Adds Polly QoS provider to Ocelot by defaults with default QoS provider. - /// - /// - /// Defaults: - /// - /// - /// - /// - /// - /// - /// Ocelot builder to extend. - /// The reference to the same extended object. - [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) - => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); - - /// - /// Creates default delegating handler based on the type. - /// - /// The downstream route to apply the handler for. - /// The context accessor of the route. - /// The factory of logger. - /// A object, but concrete type is the class. - private static DelegatingHandler GetDelegatingHandlerV7(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) - => new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory); - - #endregion -} diff --git a/src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs b/src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs deleted file mode 100644 index 162e12337..000000000 --- a/src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Diagnostics; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.Logging; -using Polly.CircuitBreaker; - -namespace Ocelot.Provider.Polly.v7; - -/// Delegates sending to downstream. -/// Outdated V7 design! Use the class. -[Obsolete("Due to new v8 policy definition in Polly 8 (use PollyResiliencePipelineDelegatingHandler)")] -public class PollyPoliciesDelegatingHandler : DelegatingHandler -{ - private readonly DownstreamRoute _route; - private readonly IHttpContextAccessor _contextAccessor; - private readonly IOcelotLogger _logger; - - public PollyPoliciesDelegatingHandler( - DownstreamRoute route, - IHttpContextAccessor contextAccessor, - IOcelotLoggerFactory loggerFactory) - { - _route = route; - _contextAccessor = contextAccessor; - _logger = loggerFactory.CreateLogger(); - } - - private IPollyQoSProvider GetQoSProvider() - { - Debug.Assert(_contextAccessor.HttpContext != null, "_contextAccessor.HttpContext != null"); - return _contextAccessor.HttpContext.RequestServices.GetService>(); - } - - /// - /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. - /// - /// Downstream request. - /// Token to cancel the task. - /// A object of a result. - /// Exception thrown when a circuit is broken. - /// Exception thrown by and classes. - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var qoSProvider = GetQoSProvider(); - - // At least one policy (timeout) will be returned - // AsyncPollyPolicy can't be null - // AsyncPollyPolicy constructor will throw if no policy is provided - var policy = qoSProvider.GetPollyPolicyWrapper(_route).AsyncPollyPolicy; - - return await policy.ExecuteAsync(async () => await base.SendAsync(request, cancellationToken)); - } -} diff --git a/src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs b/src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs deleted file mode 100644 index 821743433..000000000 --- a/src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Ocelot.Provider.Polly.v7; - -public class PollyPolicyWrapper - where TResult : class -{ - /// - /// Initializes a new instance of the class. - /// We expect at least one policy to be passed in, default can't be null. - /// - /// The policies with at least a policy. - public PollyPolicyWrapper(params IAsyncPolicy[] policies) - { - var allPolicies = policies.Where(p => p != null).ToArray(); - - AsyncPollyPolicy = allPolicies.Length > 1 ? - Policy.WrapAsync(allPolicies) : - allPolicies[0]; - } - - public IAsyncPolicy AsyncPollyPolicy { get; } -} diff --git a/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs b/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs deleted file mode 100644 index 34f76aacb..000000000 --- a/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Ocelot.Configuration; -using Ocelot.Logging; -using Polly.CircuitBreaker; -using Polly.Timeout; - -namespace Ocelot.Provider.Polly.v7; - -/// Legacy QoS provider based on Polly v7. -/// Use the as a new QoS provider based on Polly v8. -[Obsolete("Due to new v8 policy definition in Polly 8 (use PollyQoSResiliencePipelineProvider)")] -public class PollyQoSProvider : PollyQoSProviderBase, IPollyQoSProvider -{ - private readonly Dictionary> _policyWrappers = []; - - private readonly object _lockObject = new(); - private readonly IOcelotLogger _logger; - - // TODO: This should be configurable and available as global config parameter in ocelot.json - public const int DefaultRequestTimeoutSeconds = 90; - - public PollyQoSProvider(IOcelotLoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - } - - [Obsolete("Due to new v8 policy definition in Polly 8 (use GetResiliencePipeline in PollyQoSResiliencePipelineProvider)")] - public PollyPolicyWrapper GetPollyPolicyWrapper(DownstreamRoute route) - { - lock (_lockObject) - { - var currentRouteName = GetRouteName(route); - if (!_policyWrappers.ContainsKey(currentRouteName)) - { - _policyWrappers.Add(currentRouteName, PollyPolicyWrapperFactory(route)); - } - - return _policyWrappers[currentRouteName]; - } - } - - private PollyPolicyWrapper PollyPolicyWrapperFactory(DownstreamRoute route) - { - AsyncCircuitBreakerPolicy exceptionsAllowedBeforeBreakingPolicy = null; - if (route.QosOptions.ExceptionsAllowedBeforeBreaking > 0) - { - var info = $"Route: {GetRouteName(route)}; Breaker logging in {nameof(PollyQoSProvider)}: "; - - exceptionsAllowedBeforeBreakingPolicy = Policy - .HandleResult(r => ServerErrorCodes.Contains(r.StatusCode)) - .Or() - .Or() - .CircuitBreakerAsync(route.QosOptions.ExceptionsAllowedBeforeBreaking, - durationOfBreak: TimeSpan.FromMilliseconds(route.QosOptions.DurationOfBreak), - onBreak: (ex, breakDelay) => - _logger.LogError(info + $"Breaking the circuit for {breakDelay.TotalMilliseconds} ms!", - ex.Exception), - onReset: () => _logger.LogDebug(info + "Call OK! Closed the circuit again."), - onHalfOpen: () => _logger.LogDebug(info + "Half-open; Next call is a trial.")); - } - - // No default set for polly timeout at the minute. - // Since a user could potentially set timeout value = 0, we need to handle this case. - // TODO throw an exception if the user sets timeout value = 0 or at least return a warning - // TODO the design in DelegatingHandlerHandlerFactory should be reviewed - var timeoutPolicy = Policy - .TimeoutAsync( - TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue), - TimeoutStrategy.Pessimistic); - - return new PollyPolicyWrapper(exceptionsAllowedBeforeBreakingPolicy, timeoutPolicy); - } -} diff --git a/src/Ocelot/Cache/AspMemoryCache.cs b/src/Ocelot/Cache/DefaultMemoryCache.cs similarity index 94% rename from src/Ocelot/Cache/AspMemoryCache.cs rename to src/Ocelot/Cache/DefaultMemoryCache.cs index 2067b3813..f53b6ceaf 100644 --- a/src/Ocelot/Cache/AspMemoryCache.cs +++ b/src/Ocelot/Cache/DefaultMemoryCache.cs @@ -2,12 +2,12 @@ namespace Ocelot.Cache { - public class AspMemoryCache : IOcelotCache + public class DefaultMemoryCache : IOcelotCache { private readonly IMemoryCache _memoryCache; private readonly Dictionary> _regions; - public AspMemoryCache(IMemoryCache memoryCache) + public DefaultMemoryCache(IMemoryCache memoryCache) { _memoryCache = memoryCache; _regions = new Dictionary>(); diff --git a/src/Ocelot/Cache/IRegionCreator.cs b/src/Ocelot/Cache/IRegionCreator.cs deleted file mode 100644 index da1b042da..000000000 --- a/src/Ocelot/Cache/IRegionCreator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ocelot.Configuration.File; - -namespace Ocelot.Cache -{ - public interface IRegionCreator - { - string Create(FileRoute route); - } -} diff --git a/src/Ocelot/Cache/RegionCreator.cs b/src/Ocelot/Cache/RegionCreator.cs deleted file mode 100644 index c2dd0ccac..000000000 --- a/src/Ocelot/Cache/RegionCreator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Ocelot.Configuration.File; - -namespace Ocelot.Cache -{ - public class RegionCreator : IRegionCreator - { - public string Create(FileRoute route) - { - if (!string.IsNullOrEmpty(route?.FileCacheOptions?.Region)) - { - return route?.FileCacheOptions?.Region; - } - - var methods = string.Join(string.Empty, route.UpstreamHttpMethod.Select(m => m)); - - var region = $"{methods}{route.UpstreamPathTemplate.Replace("/", string.Empty)}"; - - return region; - } - } -} diff --git a/src/Ocelot/Configuration/AuthenticationOptions.cs b/src/Ocelot/Configuration/AuthenticationOptions.cs index af5cf7273..978a940a8 100644 --- a/src/Ocelot/Configuration/AuthenticationOptions.cs +++ b/src/Ocelot/Configuration/AuthenticationOptions.cs @@ -8,22 +8,21 @@ public AuthenticationOptions(List allowedScopes, string authenticationPr { AllowedScopes = allowedScopes; AuthenticationProviderKey = authenticationProviderKey; - AuthenticationProviderKeys = []; + AuthenticationProviderKeys = Array.Empty(); } public AuthenticationOptions(FileAuthenticationOptions from) { - AllowedScopes = from.AllowedScopes ?? []; + AllowedScopes = from.AllowedScopes ?? new(); AuthenticationProviderKey = from.AuthenticationProviderKey ?? string.Empty; - AuthenticationProviderKeys = from.AuthenticationProviderKeys ?? []; + AuthenticationProviderKeys = from.AuthenticationProviderKeys ?? Array.Empty(); } - public AuthenticationOptions(List allowedScopes, string authenticationProviderKey, - string[] authenticationProviderKeys) + public AuthenticationOptions(List allowedScopes, string authenticationProviderKey, string[] authenticationProviderKeys) { - AllowedScopes = allowedScopes ?? []; + AllowedScopes = allowedScopes ?? new(); AuthenticationProviderKey = authenticationProviderKey ?? string.Empty; - AuthenticationProviderKeys = authenticationProviderKeys ?? []; + AuthenticationProviderKeys = authenticationProviderKeys ?? Array.Empty(); } public List AllowedScopes { get; } diff --git a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs index e911908c7..0b85ee809 100644 --- a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs +++ b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs @@ -4,7 +4,7 @@ public class AuthenticationOptionsBuilder { private List _allowedScopes = new(); private string _authenticationProviderKey; - private string[] _authenticationProviderKeys =[]; + private string[] _authenticationProviderKeys = Array.Empty(); public AuthenticationOptionsBuilder WithAllowedScopes(List allowedScopes) { diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index 9dc93008e..2f01cd429 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -19,7 +19,7 @@ public class DownstreamRouteBuilder private List _claimToDownstreamPath; private string _requestIdHeaderKey; private bool _isCached; - private CacheOptions _fileCacheOptions; + private CacheOptions _cacheOptions; private string _downstreamScheme; private LoadBalancerOptions _loadBalancerOptions; private QoSOptions _qosOptions; @@ -40,13 +40,16 @@ public class DownstreamRouteBuilder private SecurityOptions _securityOptions; private string _downstreamHttpMethod; private Version _downstreamHttpVersion; + private HttpVersionPolicy _downstreamHttpVersionPolicy; + private Dictionary _upstreamHeaders; + private MetadataOptions _metadataOptions; public DownstreamRouteBuilder() { - _downstreamAddresses = new List(); - _delegatingHandlers = new List(); - _addHeadersToDownstream = new List(); - _addHeadersToUpstream = new List(); + _downstreamAddresses = new(); + _delegatingHandlers = new(); + _addHeadersToDownstream = new(); + _addHeadersToUpstream = new(); } public DownstreamRouteBuilder WithDownstreamAddresses(List downstreamAddresses) @@ -87,7 +90,9 @@ public DownstreamRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate inpu public DownstreamRouteBuilder WithUpstreamHttpMethod(List input) { - _upstreamHttpMethod = (input.Count == 0) ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); + _upstreamHttpMethod = input.Count > 0 + ? input.Select(x => new HttpMethod(x.Trim())).ToList() + : new(); return this; } @@ -147,7 +152,7 @@ public DownstreamRouteBuilder WithIsCached(bool input) public DownstreamRouteBuilder WithCacheOptions(CacheOptions input) { - _fileCacheOptions = input; + _cacheOptions = input; return this; } @@ -259,6 +264,24 @@ public DownstreamRouteBuilder WithDownstreamHttpVersion(Version downstreamHttpVe return this; } + public DownstreamRouteBuilder WithUpstreamHeaders(Dictionary input) + { + _upstreamHeaders = input; + return this; + } + + public DownstreamRouteBuilder WithDownstreamHttpVersionPolicy(HttpVersionPolicy downstreamHttpVersionPolicy) + { + _downstreamHttpVersionPolicy = downstreamHttpVersionPolicy; + return this; + } + + public DownstreamRouteBuilder WithMetadata(MetadataOptions metadataOptions) + { + _metadataOptions = metadataOptions; + return this; + } + public DownstreamRoute Build() { return new DownstreamRoute( @@ -276,7 +299,7 @@ public DownstreamRoute Build() _downstreamScheme, _requestIdHeaderKey, _isCached, - _fileCacheOptions, + _cacheOptions, _loadBalancerOptions, _rateLimitOptions, _routeClaimRequirement, @@ -295,6 +318,9 @@ public DownstreamRoute Build() _dangerousAcceptAnyServerCertificateValidator, _securityOptions, _downstreamHttpMethod, - _downstreamHttpVersion); + _downstreamHttpVersion, + _downstreamHttpVersionPolicy, + _upstreamHeaders, + _metadataOptions); } } diff --git a/src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs new file mode 100644 index 000000000..79b765672 --- /dev/null +++ b/src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs @@ -0,0 +1,54 @@ +using System.Globalization; + +namespace Ocelot.Configuration.Builder; + +public class MetadataOptionsBuilder +{ + private string[] _separators; + private char[] _trimChars; + private StringSplitOptions _stringSplitOption; + private NumberStyles _numberStyle; + private CultureInfo _currentCulture; + private IDictionary _metadata; + + public MetadataOptionsBuilder WithSeparators(string[] separators) + { + _separators = separators; + return this; + } + + public MetadataOptionsBuilder WithTrimChars(char[] trimChars) + { + _trimChars = trimChars; + return this; + } + + public MetadataOptionsBuilder WithStringSplitOption(string stringSplitOption) + { + _stringSplitOption = Enum.Parse(stringSplitOption); + return this; + } + + public MetadataOptionsBuilder WithNumberStyle(string numberStyle) + { + _numberStyle = Enum.Parse(numberStyle); + return this; + } + + public MetadataOptionsBuilder WithCurrentCulture(string currentCulture) + { + _currentCulture = CultureInfo.GetCultureInfo(currentCulture); + return this; + } + + public MetadataOptionsBuilder WithMetadata(IDictionary metadata) + { + _metadata = metadata; + return this; + } + + public MetadataOptions Build() + { + return new MetadataOptions(_separators, _trimChars, _stringSplitOption, _numberStyle, _currentCulture, _metadata); + } +} diff --git a/src/Ocelot/Configuration/Builder/RouteBuilder.cs b/src/Ocelot/Configuration/Builder/RouteBuilder.cs index 8e7614a9c..a731b3b3b 100644 --- a/src/Ocelot/Configuration/Builder/RouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/RouteBuilder.cs @@ -10,7 +10,8 @@ public class RouteBuilder private string _upstreamHost; private List _downstreamRoutes; private List _downstreamRoutesConfig; - private string _aggregator; + private string _aggregator; + private IDictionary _upstreamHeaders; public RouteBuilder() { @@ -58,6 +59,12 @@ public RouteBuilder WithAggregator(string aggregator) { _aggregator = aggregator; return this; + } + + public RouteBuilder WithUpstreamHeaders(IDictionary upstreamHeaders) + { + _upstreamHeaders = upstreamHeaders; + return this; } public Route Build() @@ -68,8 +75,8 @@ public Route Build() _upstreamHttpMethod, _upstreamTemplatePattern, _upstreamHost, - _aggregator - ); + _aggregator, + _upstreamHeaders); } } } diff --git a/src/Ocelot/Configuration/CacheOptions.cs b/src/Ocelot/Configuration/CacheOptions.cs index 352b501d8..dc3c19116 100644 --- a/src/Ocelot/Configuration/CacheOptions.cs +++ b/src/Ocelot/Configuration/CacheOptions.cs @@ -1,39 +1,42 @@ using Ocelot.Request.Middleware; -namespace Ocelot.Configuration -{ - public class CacheOptions - { - internal CacheOptions() { } +namespace Ocelot.Configuration; - public CacheOptions(int ttlSeconds, string region, string header) - { - TtlSeconds = ttlSeconds; - Region = region; - Header = header; - } - - public CacheOptions(int ttlSeconds, string region, string header, bool enableContentHashing) - { - TtlSeconds = ttlSeconds; - Region = region; - Header = header; - EnableContentHashing = enableContentHashing; - } +public class CacheOptions +{ + internal CacheOptions() { } - public int TtlSeconds { get; } - public string Region { get; } - public string Header { get; } - - /// - /// Enables MD5 hash calculation of the of the object. - /// - /// - /// Default value is . No hashing by default. - /// - /// - /// if hashing is enabled, otherwise it is . - /// - public bool EnableContentHashing { get; } + /// + /// Initializes a new instance of the class. + /// + /// + /// Internal defaults: + /// + /// The default value for is , but it is set to null for route-level configuration to allow global configuration usage. + /// The default value for is 0. + /// + /// + /// Time-to-live seconds. If not speciefied, zero value is used by default. + /// The region of caching. + /// The header name to control cached value. + /// The switcher for content hashing. If not speciefied, false value is used by default. + public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing) + { + TtlSeconds = ttlSeconds ?? 0; + Region = region; + Header = header; + EnableContentHashing = enableContentHashing ?? false; } -} + + /// Time-to-live seconds. + /// Default value is 0. No caching by default. + /// An value of seconds. + public int TtlSeconds { get; } + public string Region { get; } + public string Header { get; } + + /// Enables MD5 hash calculation of the of the object. + /// Default value is . No hashing by default. + /// if hashing is enabled, otherwise it is . + public bool EnableContentHashing { get; } +} diff --git a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs index 9a5ae2906..24d3a58b3 100644 --- a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs +++ b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs @@ -5,11 +5,13 @@ namespace Ocelot.Configuration.Creator { public class AggregatesCreator : IAggregatesCreator { - private readonly IUpstreamTemplatePatternCreator _creator; + private readonly IUpstreamTemplatePatternCreator _creator; + private readonly IUpstreamHeaderTemplatePatternCreator _headerCreator; - public AggregatesCreator(IUpstreamTemplatePatternCreator creator) + public AggregatesCreator(IUpstreamTemplatePatternCreator creator, IUpstreamHeaderTemplatePatternCreator headerCreator) { - _creator = creator; + _creator = creator; + _headerCreator = headerCreator; } public List Create(FileConfiguration fileConfiguration, List routes) @@ -35,7 +37,8 @@ private Route SetUpAggregateRoute(IEnumerable routes, FileAggregateRoute applicableRoutes.Add(downstreamRoute); } - var upstreamTemplatePattern = _creator.Create(aggregateRoute); + var upstreamTemplatePattern = _creator.Create(aggregateRoute); + var upstreamHeaderTemplates = _headerCreator.Create(aggregateRoute); var route = new RouteBuilder() .WithUpstreamHttpMethod(aggregateRoute.UpstreamHttpMethod) @@ -43,7 +46,8 @@ private Route SetUpAggregateRoute(IEnumerable routes, FileAggregateRoute .WithDownstreamRoutes(applicableRoutes) .WithAggregateRouteConfig(aggregateRoute.RouteKeysConfig) .WithUpstreamHost(aggregateRoute.UpstreamHost) - .WithAggregator(aggregateRoute.Aggregator) + .WithAggregator(aggregateRoute.Aggregator) + .WithUpstreamHeaders(upstreamHeaderTemplates) .Build(); return route; diff --git a/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs new file mode 100644 index 000000000..6d1c8f2b4 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs @@ -0,0 +1,27 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +public class CacheOptionsCreator : ICacheOptionsCreator +{ + public CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration global, string upstreamPathTemplate, IList upstreamHttpMethods) + { + var region = GetRegion(options.Region ?? global?.CacheOptions.Region, upstreamPathTemplate, upstreamHttpMethods); + var header = options.Header ?? global?.CacheOptions.Header; + var ttlSeconds = options.TtlSeconds ?? global?.CacheOptions.TtlSeconds; + var enableContentHashing = options.EnableContentHashing ?? global?.CacheOptions.EnableContentHashing; + + return new CacheOptions(ttlSeconds, region, header, enableContentHashing); + } + + protected virtual string GetRegion(string region, string upstreamPathTemplate, IList upstreamHttpMethod) + { + if (!string.IsNullOrEmpty(region)) + { + return region; + } + + var methods = string.Join(string.Empty, upstreamHttpMethod); + return $"{methods}{upstreamPathTemplate.Replace("/", string.Empty)}"; + } +} diff --git a/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs index 694292ad4..50c4f9490 100644 --- a/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs @@ -12,6 +12,7 @@ public class ConfigurationCreator : IConfigurationCreator private readonly IAdministrationPath _adminPath; private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; private readonly IVersionCreator _versionCreator; + private readonly IVersionPolicyCreator _versionPolicyCreator; public ConfigurationCreator( IServiceProviderConfigurationCreator serviceProviderConfigCreator, @@ -19,7 +20,8 @@ public ConfigurationCreator( IHttpHandlerOptionsCreator httpHandlerOptionsCreator, IServiceProvider serviceProvider, ILoadBalancerOptionsCreator loadBalancerOptionsCreator, - IVersionCreator versionCreator + IVersionCreator versionCreator, + IVersionPolicyCreator versionPolicyCreator ) { _adminPath = serviceProvider.GetService(); @@ -28,6 +30,7 @@ IVersionCreator versionCreator _qosOptionsCreator = qosOptionsCreator; _httpHandlerOptionsCreator = httpHandlerOptionsCreator; _versionCreator = versionCreator; + _versionPolicyCreator = versionPolicyCreator; } public InternalConfiguration Create(FileConfiguration fileConfiguration, List routes) @@ -43,6 +46,8 @@ public InternalConfiguration Create(FileConfiguration fileConfiguration, List +/// This class implements the interface. +/// +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); + + foreach (var (key, value) in metadata) + { + mergedMetadata[key] = value; + } + + return new MetadataOptionsBuilder() + .WithMetadata(mergedMetadata) + .WithSeparators(options.Separators) + .WithTrimChars(options.TrimChars) + .WithStringSplitOption(options.StringSplitOption) + .WithNumberStyle(options.NumberStyle) + .WithCurrentCulture(options.CurrentCulture) + .Build(); + } +} diff --git a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs index ca090fb35..cb5cdf419 100644 --- a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs +++ b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs @@ -7,11 +7,19 @@ public class DynamicsCreator : IDynamicsCreator { private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; private readonly IVersionCreator _versionCreator; - - public DynamicsCreator(IRateLimitOptionsCreator rateLimitOptionsCreator, IVersionCreator versionCreator) + private readonly IVersionPolicyCreator _versionPolicyCreator; + private readonly IMetadataCreator _metadataCreator; + + public DynamicsCreator( + IRateLimitOptionsCreator rateLimitOptionsCreator, + IVersionCreator versionCreator, + IVersionPolicyCreator versionPolicyCreator, + IMetadataCreator metadataCreator) { _rateLimitOptionsCreator = rateLimitOptionsCreator; _versionCreator = versionCreator; + _versionPolicyCreator = versionPolicyCreator; + _metadataCreator = metadataCreator; } public List Create(FileConfiguration fileConfiguration) @@ -27,12 +35,16 @@ private Route SetUpDynamicRoute(FileDynamicRoute fileDynamicRoute, FileGlobalCon .Create(fileDynamicRoute.RateLimitRule, globalConfiguration); var version = _versionCreator.Create(fileDynamicRoute.DownstreamHttpVersion); + var versionPolicy = _versionPolicyCreator.Create(fileDynamicRoute.DownstreamHttpVersionPolicy); + var metadata = _metadataCreator.Create(fileDynamicRoute.Metadata, globalConfiguration); var downstreamRoute = new DownstreamRouteBuilder() .WithEnableRateLimiting(rateLimitOption.EnableRateLimiting) .WithRateLimitOptions(rateLimitOption) .WithServiceName(fileDynamicRoute.ServiceName) .WithDownstreamHttpVersion(version) + .WithDownstreamHttpVersionPolicy(versionPolicy) + .WithMetadata(metadata) .Build(); var route = new RouteBuilder() diff --git a/src/Ocelot/Configuration/Creator/HttpVersionPolicyCreator.cs b/src/Ocelot/Configuration/Creator/HttpVersionPolicyCreator.cs new file mode 100644 index 000000000..0c8af7123 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/HttpVersionPolicyCreator.cs @@ -0,0 +1,20 @@ +namespace Ocelot.Configuration.Creator; + +/// +/// Default implementation of the interface. +/// +public class HttpVersionPolicyCreator : IVersionPolicyCreator +{ + /// + /// Creates a by a string. + /// + /// The string representation of the version policy. + /// An enumeration value. + public HttpVersionPolicy Create(string downstreamHttpVersionPolicy) => downstreamHttpVersionPolicy switch + { + VersionPolicies.RequestVersionExact => HttpVersionPolicy.RequestVersionExact, + VersionPolicies.RequestVersionOrHigher => HttpVersionPolicy.RequestVersionOrHigher, + VersionPolicies.RequestVersionOrLower => HttpVersionPolicy.RequestVersionOrLower, + _ => HttpVersionPolicy.RequestVersionOrLower, + }; +} diff --git a/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs b/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs new file mode 100644 index 000000000..a76a1b20e --- /dev/null +++ b/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs @@ -0,0 +1,19 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +/// +/// This interface is used to create cache options. +/// +public interface ICacheOptionsCreator +{ + /// + /// Creates cache options based on the file cache options, upstream path template and upstream HTTP methods. + /// Upstream path template and upstream HTTP methods are used to get the region name. + /// The file cache options. + /// The global configuration. + /// 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); +} diff --git a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs new file mode 100644 index 000000000..5252d620b --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs @@ -0,0 +1,11 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +/// +/// This interface describes the creation of metadata options. +/// +public interface IMetadataCreator +{ + MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration globalConfiguration); +} diff --git a/src/Ocelot/Configuration/Creator/IUpstreamHeaderTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/IUpstreamHeaderTemplatePatternCreator.cs new file mode 100644 index 000000000..d2fea8004 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IUpstreamHeaderTemplatePatternCreator.cs @@ -0,0 +1,17 @@ +using Ocelot.Configuration.File; +using Ocelot.Values; + +namespace Ocelot.Configuration.Creator; + +/// +/// Ocelot feature: Routing based on request header. +/// +public interface IUpstreamHeaderTemplatePatternCreator +{ + /// + /// Creates upstream templates based on route headers. + /// + /// The route info. + /// An object where TKey is , TValue is . + IDictionary Create(IRoute route); +} diff --git a/src/Ocelot/Configuration/Creator/IVersionPolicyCreator.cs b/src/Ocelot/Configuration/Creator/IVersionPolicyCreator.cs new file mode 100644 index 000000000..3d25d8cea --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IVersionPolicyCreator.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Configuration.Creator; + +/// +/// Defines conversions from version policy strings to enumeration values. +/// +public interface IVersionPolicyCreator +{ + /// + /// Creates a by a string. + /// + /// The string representation of the version policy. + /// An enumeration value. + HttpVersionPolicy Create(string downstreamHttpVersionPolicy); +} diff --git a/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs index 8e0911e56..8b6f0e3ea 100644 --- a/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs @@ -17,6 +17,8 @@ public RouteOptions Create(FileRoute fileRoute) && (!string.IsNullOrEmpty(authOpts.AuthenticationProviderKey) || authOpts.AuthenticationProviderKeys?.Any(k => !string.IsNullOrWhiteSpace(k)) == true); var isAuthorized = fileRoute.RouteClaimsRequirement?.Any() == true; + + // TODO: This sounds more like a hack, it might be better to refactor this at some point. var isCached = fileRoute.FileCacheOptions.TtlSeconds > 0; var enableRateLimiting = fileRoute.RateLimitOptions?.EnableRateLimiting == true; var useServiceDiscovery = !string.IsNullOrEmpty(fileRoute.ServiceName); diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs index 8c1f1de63..015944014 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -1,4 +1,3 @@ -using Ocelot.Cache; using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; @@ -10,17 +9,20 @@ public class RoutesCreator : IRoutesCreator 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 IRegionCreator _regionCreator; + 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, @@ -30,21 +32,23 @@ public RoutesCreator( IQoSOptionsCreator qosOptionsCreator, IRouteOptionsCreator fileRouteOptionsCreator, IRateLimitOptionsCreator rateLimitOptionsCreator, - IRegionCreator regionCreator, + ICacheOptionsCreator cacheOptionsCreator, IHttpHandlerOptionsCreator httpHandlerOptionsCreator, IHeaderFindAndReplaceCreator headerFAndRCreator, IDownstreamAddressesCreator downstreamAddressesCreator, ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IRouteKeyCreator routeKeyCreator, ISecurityOptionsCreator securityOptionsCreator, - IVersionCreator versionCreator - ) + IVersionCreator versionCreator, + IVersionPolicyCreator versionPolicyCreator, + IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator, + IMetadataCreator metadataCreator) { _routeKeyCreator = routeKeyCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _downstreamAddressesCreator = downstreamAddressesCreator; _headerFAndRCreator = headerFAndRCreator; - _regionCreator = regionCreator; + _cacheOptionsCreator = cacheOptionsCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator; _requestIdKeyCreator = requestIdKeyCreator; _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; @@ -56,6 +60,9 @@ IVersionCreator versionCreator _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _securityOptionsCreator = securityOptionsCreator; _versionCreator = versionCreator; + _versionPolicyCreator = versionPolicyCreator; + _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; + _metadataCreator = metadataCreator; } public List Create(FileConfiguration fileConfiguration) @@ -93,8 +100,6 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf var rateLimitOption = _rateLimitOptionsCreator.Create(fileRoute.RateLimitOptions, globalConfiguration); - var region = _regionCreator.Create(fileRoute); - var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileRoute.HttpHandlerOptions); var hAndRs = _headerFAndRCreator.Create(fileRoute); @@ -107,6 +112,12 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf var downstreamHttpVersion = _versionCreator.Create(fileRoute.DownstreamHttpVersion); + var downstreamHttpVersionPolicy = _versionPolicyCreator.Create(fileRoute.DownstreamHttpVersionPolicy); + + 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) @@ -122,7 +133,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf .WithClaimsToDownstreamPath(claimsToDownstreamPath) .WithRequestIdKey(requestIdKey) .WithIsCached(fileRouteOptions.IsCached) - .WithCacheOptions(new CacheOptions(fileRoute.FileCacheOptions.TtlSeconds, region, fileRoute.FileCacheOptions.Header)) + .WithCacheOptions(cacheOptions) .WithDownstreamScheme(fileRoute.DownstreamScheme) .WithLoadBalancerOptions(lbOptions) .WithDownstreamAddresses(downstreamAddresses) @@ -142,7 +153,9 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf .WithDangerousAcceptAnyServerCertificateValidator(fileRoute.DangerousAcceptAnyServerCertificateValidator) .WithSecurityOptions(securityOptions) .WithDownstreamHttpVersion(downstreamHttpVersion) + .WithDownstreamHttpVersionPolicy(downstreamHttpVersionPolicy) .WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod) + .WithMetadata(metadata) .Build(); return route; @@ -151,12 +164,14 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoutes) { var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); + var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute); var route = new RouteBuilder() .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod) .WithUpstreamPathTemplate(upstreamTemplatePattern) .WithDownstreamRoute(downstreamRoutes) .WithUpstreamHost(fileRoute.UpstreamHost) + .WithUpstreamHeaders(upstreamHeaderTemplates) .Build(); return route; diff --git a/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs new file mode 100644 index 000000000..52c653f5e --- /dev/null +++ b/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs @@ -0,0 +1,50 @@ +using Ocelot.Configuration.File; +using Ocelot.Values; + +namespace Ocelot.Configuration.Creator; + +/// +/// Default creator of upstream templates based on route headers. +/// +/// Ocelot feature: Routing based on request header. +public partial class UpstreamHeaderTemplatePatternCreator : IUpstreamHeaderTemplatePatternCreator +{ + private const string PlaceHolderPattern = @"(\{header:.*?\})"; +#if NET7_0_OR_GREATER + [GeneratedRegex(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")] + private static partial Regex RegExPlaceholders(); +#else + private static readonly Regex RegExPlaceholdersVar = new(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000)); + private static Regex RegExPlaceholders() => RegExPlaceholdersVar; +#endif + + public IDictionary Create(IRoute route) + { + var result = new Dictionary(); + + foreach (var headerTemplate in route.UpstreamHeaderTemplates) + { + var headerTemplateValue = headerTemplate.Value; + var matches = RegExPlaceholders().Matches(headerTemplateValue); + + if (matches.Count > 0) + { + var placeholders = matches.Select(m => m.Groups[1].Value).ToArray(); + for (int i = 0; i < placeholders.Length; i++) + { + var indexOfPlaceholder = headerTemplateValue.IndexOf(placeholders[i]); + var placeholderName = placeholders[i][8..^1]; // remove "{header:" and "}" + headerTemplateValue = headerTemplateValue.Replace(placeholders[i], $"(?<{placeholderName}>.+)"); + } + } + + var template = route.RouteIsCaseSensitive + ? $"^{headerTemplateValue}$" + : $"^(?i){headerTemplateValue}$"; // ignore case + + result.Add(headerTemplate.Key, new(template, headerTemplate.Value)); + } + + return result; + } +} diff --git a/src/Ocelot/Configuration/Creator/VersionPolicies.cs b/src/Ocelot/Configuration/Creator/VersionPolicies.cs new file mode 100644 index 000000000..8073d47ab --- /dev/null +++ b/src/Ocelot/Configuration/Creator/VersionPolicies.cs @@ -0,0 +1,11 @@ +namespace Ocelot.Configuration.Creator; + +/// +/// Constants for conversions in concrete classes for the interface. +/// +public class VersionPolicies +{ + public const string RequestVersionExact = nameof(RequestVersionExact); + public const string RequestVersionOrLower = nameof(RequestVersionOrLower); + public const string RequestVersionOrHigher = nameof(RequestVersionOrHigher); +} diff --git a/src/Ocelot/Configuration/DownstreamRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs index 585c2554f..0241f9b61 100644 --- a/src/Ocelot/Configuration/DownstreamRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -39,7 +39,10 @@ public DownstreamRoute( bool dangerousAcceptAnyServerCertificateValidator, SecurityOptions securityOptions, string downstreamHttpMethod, - Version downstreamHttpVersion) + Version downstreamHttpVersion, + HttpVersionPolicy downstreamHttpVersionPolicy, + Dictionary upstreamHeaders, + MetadataOptions metadataOptions) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; AddHeadersToDownstream = addHeadersToDownstream; @@ -74,7 +77,10 @@ public DownstreamRoute( AddHeadersToUpstream = addHeadersToUpstream; SecurityOptions = securityOptions; DownstreamHttpMethod = downstreamHttpMethod; - DownstreamHttpVersion = downstreamHttpVersion; + DownstreamHttpVersion = downstreamHttpVersion; + DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; + UpstreamHeaders = upstreamHeaders ?? new(); + MetadataOptions = metadataOptions; } public string Key { get; } @@ -110,6 +116,20 @@ public DownstreamRoute( public bool DangerousAcceptAnyServerCertificateValidator { get; } public SecurityOptions SecurityOptions { get; } public string DownstreamHttpMethod { get; } - public Version DownstreamHttpVersion { get; } + public Version DownstreamHttpVersion { get; } + + /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. + /// An enum value being mapped from a constant. + /// + /// Related to the property. + /// + /// HttpVersionPolicy Enum + /// HttpVersion Class + /// HttpRequestMessage.VersionPolicy Property + /// + /// + public HttpVersionPolicy DownstreamHttpVersionPolicy { get; } + public Dictionary UpstreamHeaders { get; } + public MetadataOptions MetadataOptions { get; } } } diff --git a/src/Ocelot/Configuration/File/FileAggregateRoute.cs b/src/Ocelot/Configuration/File/FileAggregateRoute.cs index ad47d735f..fa0ef305a 100644 --- a/src/Ocelot/Configuration/File/FileAggregateRoute.cs +++ b/src/Ocelot/Configuration/File/FileAggregateRoute.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; + namespace Ocelot.Configuration.File { public class FileAggregateRoute : IRoute @@ -10,8 +12,15 @@ public class FileAggregateRoute : IRoute public string Aggregator { get; set; } // Only supports GET..are you crazy!! POST, PUT WOULD BE CRAZY!! :) - public List UpstreamHttpMethod => new() { "Get" }; - - public int Priority { get; set; } = 1; + public List UpstreamHttpMethod => new() { HttpMethods.Get }; + public IDictionary UpstreamHeaderTemplates { get; set; } + public int Priority { get; set; } = 1; + + public FileAggregateRoute() + { + RouteKeys = new(); + RouteKeysConfig = new(); + UpstreamHeaderTemplates = new Dictionary(); + } } } diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs index 24d9b787d..0dd93a9c6 100644 --- a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -4,13 +4,13 @@ public sealed class FileAuthenticationOptions { public FileAuthenticationOptions() { - AllowedScopes = []; - AuthenticationProviderKeys = []; + AllowedScopes = new(); + AuthenticationProviderKeys = Array.Empty(); } public FileAuthenticationOptions(FileAuthenticationOptions from) { - AllowedScopes = [..from.AllowedScopes]; + AllowedScopes = new(from.AllowedScopes); AuthenticationProviderKey = from.AuthenticationProviderKey; AuthenticationProviderKeys = from.AuthenticationProviderKeys; } diff --git a/src/Ocelot/Configuration/File/FileCacheOptions.cs b/src/Ocelot/Configuration/File/FileCacheOptions.cs index a1b1deed5..42b793390 100644 --- a/src/Ocelot/Configuration/File/FileCacheOptions.cs +++ b/src/Ocelot/Configuration/File/FileCacheOptions.cs @@ -1,21 +1,26 @@ -namespace Ocelot.Configuration.File -{ - public class FileCacheOptions - { - public FileCacheOptions() - { - Region = string.Empty; - TtlSeconds = 0; - } +namespace Ocelot.Configuration.File; - public FileCacheOptions(FileCacheOptions from) - { - Region = from.Region; - TtlSeconds = from.TtlSeconds; - } +public class FileCacheOptions +{ + public FileCacheOptions() { } - public int TtlSeconds { get; set; } - public string Region { get; set; } - public string Header { get; set; } - } + public FileCacheOptions(FileCacheOptions from) + { + Region = from.Region; + TtlSeconds = from.TtlSeconds; + Header = from.Header; + EnableContentHashing = from.EnableContentHashing; + } + + /// Using where T is to have as default value and allowing global configuration usage. + /// If then use global configuration with 0 by default. + /// The time to live seconds, with 0 by default. + public int? TtlSeconds { get; set; } + public string Region { get; set; } + public string Header { get; set; } + + /// Using where T is to have as default value and allowing global configuration usage. + /// If then use global configuration with by default. + /// if content hashing is enabled; otherwise, . + public bool? EnableContentHashing { get; set; } } diff --git a/src/Ocelot/Configuration/File/FileDynamicRoute.cs b/src/Ocelot/Configuration/File/FileDynamicRoute.cs index fb93c2f91..a42581531 100644 --- a/src/Ocelot/Configuration/File/FileDynamicRoute.cs +++ b/src/Ocelot/Configuration/File/FileDynamicRoute.cs @@ -1,3 +1,5 @@ +using Ocelot.Configuration.Creator; + namespace Ocelot.Configuration.File { public class FileDynamicRoute @@ -5,5 +7,18 @@ public class FileDynamicRoute public string ServiceName { get; set; } public FileRateLimitRule RateLimitRule { 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 IDictionary Metadata { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 6692ba046..7ce35f99e 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -1,4 +1,6 @@ -namespace Ocelot.Configuration.File +using Ocelot.Configuration.Creator; + +namespace Ocelot.Configuration.File { public class FileGlobalConfiguration { @@ -9,6 +11,8 @@ public FileGlobalConfiguration() LoadBalancerOptions = new FileLoadBalancerOptions(); QoSOptions = new FileQoSOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); + CacheOptions = new FileCacheOptions(); + MetadataOptions = new FileMetadataOptions(); } public string RequestIdKey { get; set; } @@ -28,5 +32,21 @@ public FileGlobalConfiguration() public FileHttpHandlerOptions HttpHandlerOptions { 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 FileCacheOptions CacheOptions { get; set; } + + public FileMetadataOptions MetadataOptions { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileMetadataOptions.cs b/src/Ocelot/Configuration/File/FileMetadataOptions.cs new file mode 100644 index 000000000..8d34bbe44 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileMetadataOptions.cs @@ -0,0 +1,33 @@ +using System.Globalization; + +namespace Ocelot.Configuration.File; + +public class FileMetadataOptions +{ + public FileMetadataOptions() + { + Separators = new[] { "," }; + TrimChars = new[] { ' ' }; + StringSplitOption = Enum.GetName(typeof(StringSplitOptions), StringSplitOptions.None); + NumberStyle = Enum.GetName(typeof(NumberStyles), NumberStyles.Any); + CurrentCulture = CultureInfo.CurrentCulture.Name; + Metadata = new Dictionary(); + } + + public FileMetadataOptions(FileMetadataOptions from) + { + Separators = from.Separators; + TrimChars = from.TrimChars; + StringSplitOption = from.StringSplitOption; + NumberStyle = from.NumberStyle; + CurrentCulture = from.CurrentCulture; + Metadata = from.Metadata; + } + + public IDictionary Metadata { get; set; } + public string[] Separators { get; set; } + public char[] TrimChars { get; set; } + public string StringSplitOption { get; set; } + public string NumberStyle { get; set; } + public string CurrentCulture { get; set; } +} diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index 5823113ad..b9a3c4eda 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -1,4 +1,6 @@ -namespace Ocelot.Configuration.File +using Ocelot.Configuration.Creator; + +namespace Ocelot.Configuration.File { public class FileRoute : IRoute, ICloneable { @@ -15,11 +17,13 @@ public FileRoute() FileCacheOptions = new FileCacheOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); LoadBalancerOptions = new FileLoadBalancerOptions(); + Metadata = new Dictionary(); Priority = 1; QoSOptions = new FileQoSOptions(); RateLimitOptions = new FileRateLimitRule(); RouteClaimsRequirement = new Dictionary(); SecurityOptions = new FileSecurityOptions(); + UpstreamHeaderTemplates = new Dictionary(); UpstreamHeaderTransform = new Dictionary(); UpstreamHttpMethod = new List(); } @@ -40,12 +44,25 @@ public FileRoute(FileRoute from) public List DownstreamHostAndPorts { 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. + /// + /// Related to the property. + /// + /// HttpVersionPolicy Enum + /// HttpVersion Class + /// HttpRequestMessage.VersionPolicy Property + /// + /// + public string DownstreamHttpVersionPolicy { get; set; } public string DownstreamPathTemplate { get; set; } - public string DownstreamScheme { get; set; } + public string DownstreamScheme { get; set; } public FileCacheOptions FileCacheOptions { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public string Key { get; set; } public FileLoadBalancerOptions LoadBalancerOptions { get; set; } + public IDictionary Metadata { get; set; } public int Priority { get; set; } public FileQoSOptions QoSOptions { get; set; } public FileRateLimitRule RateLimitOptions { get; set; } @@ -60,6 +77,7 @@ public FileRoute(FileRoute from) public string UpstreamHost { get; set; } public List UpstreamHttpMethod { get; set; } public string UpstreamPathTemplate { get; set; } + public IDictionary UpstreamHeaderTemplates { get; set; } /// /// Clones this object by making a deep copy. @@ -85,12 +103,14 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.DownstreamHostAndPorts = from.DownstreamHostAndPorts.Select(x => new FileHostAndPort(x)).ToList(); to.DownstreamHttpMethod = from.DownstreamHttpMethod; to.DownstreamHttpVersion = from.DownstreamHttpVersion; + to.DownstreamHttpVersionPolicy = from.DownstreamHttpVersionPolicy; to.DownstreamPathTemplate = from.DownstreamPathTemplate; - to.DownstreamScheme = from.DownstreamScheme; + to.DownstreamScheme = from.DownstreamScheme; to.FileCacheOptions = new(from.FileCacheOptions); to.HttpHandlerOptions = new(from.HttpHandlerOptions); to.Key = from.Key; to.LoadBalancerOptions = new(from.LoadBalancerOptions); + to.Metadata = new Dictionary(from.Metadata); to.Priority = from.Priority; to.QoSOptions = new(from.QoSOptions); to.RateLimitOptions = new(from.RateLimitOptions); @@ -101,6 +121,7 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.ServiceName = from.ServiceName; to.ServiceNamespace = from.ServiceNamespace; to.Timeout = from.Timeout; + to.UpstreamHeaderTemplates = new Dictionary(from.UpstreamHeaderTemplates); to.UpstreamHeaderTransform = new(from.UpstreamHeaderTransform); to.UpstreamHost = from.UpstreamHost; to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); diff --git a/src/Ocelot/Configuration/File/IRoute.cs b/src/Ocelot/Configuration/File/IRoute.cs index 74df79b23..1a70debb3 100644 --- a/src/Ocelot/Configuration/File/IRoute.cs +++ b/src/Ocelot/Configuration/File/IRoute.cs @@ -2,6 +2,7 @@ public interface IRoute { + IDictionary UpstreamHeaderTemplates { get; set; } string UpstreamPathTemplate { get; set; } bool RouteIsCaseSensitive { get; set; } int Priority { get; set; } diff --git a/src/Ocelot/Configuration/IInternalConfiguration.cs b/src/Ocelot/Configuration/IInternalConfiguration.cs index da839cf8b..59ef78549 100644 --- a/src/Ocelot/Configuration/IInternalConfiguration.cs +++ b/src/Ocelot/Configuration/IInternalConfiguration.cs @@ -1,3 +1,5 @@ +using Ocelot.Configuration.File; + namespace Ocelot.Configuration { public interface IInternalConfiguration @@ -19,5 +21,9 @@ public interface IInternalConfiguration HttpHandlerOptions HttpHandlerOptions { get; } Version DownstreamHttpVersion { get; } + + /// Global HTTP version policy. It is related to property. + /// An enumeration value. + HttpVersionPolicy? DownstreamHttpVersionPolicy { get; } } } diff --git a/src/Ocelot/Configuration/InternalConfiguration.cs b/src/Ocelot/Configuration/InternalConfiguration.cs index 86eb5472b..7d047cfe6 100644 --- a/src/Ocelot/Configuration/InternalConfiguration.cs +++ b/src/Ocelot/Configuration/InternalConfiguration.cs @@ -1,3 +1,5 @@ +using Ocelot.Configuration.File; + namespace Ocelot.Configuration { public class InternalConfiguration : IInternalConfiguration @@ -11,7 +13,8 @@ public InternalConfiguration( string downstreamScheme, QoSOptions qoSOptions, HttpHandlerOptions httpHandlerOptions, - Version downstreamHttpVersion) + Version downstreamHttpVersion, + HttpVersionPolicy? downstreamHttpVersionPolicy) { Routes = routes; AdministrationPath = administrationPath; @@ -22,6 +25,7 @@ public InternalConfiguration( QoSOptions = qoSOptions; HttpHandlerOptions = httpHandlerOptions; DownstreamHttpVersion = downstreamHttpVersion; + DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; } public List Routes { get; } @@ -34,5 +38,9 @@ public InternalConfiguration( public HttpHandlerOptions HttpHandlerOptions { get; } public Version DownstreamHttpVersion { get; } + + /// Global HTTP version policy. It is related to property. + /// An enumeration value. + public HttpVersionPolicy? DownstreamHttpVersionPolicy { get; } } } diff --git a/src/Ocelot/Configuration/MetadataOptions.cs b/src/Ocelot/Configuration/MetadataOptions.cs new file mode 100644 index 000000000..b94a1fb7c --- /dev/null +++ b/src/Ocelot/Configuration/MetadataOptions.cs @@ -0,0 +1,45 @@ +using Ocelot.Configuration.File; +using System.Globalization; + +namespace Ocelot.Configuration; + +public class MetadataOptions +{ + public MetadataOptions(MetadataOptions from) + { + Separators = from.Separators; + TrimChars = from.TrimChars; + StringSplitOption = from.StringSplitOption; + NumberStyle = from.NumberStyle; + CurrentCulture = from.CurrentCulture; + Metadata = from.Metadata; + } + + public MetadataOptions(FileMetadataOptions from) + { + StringSplitOption = Enum.Parse(from.StringSplitOption); + NumberStyle = Enum.Parse(from.NumberStyle); + CurrentCulture = CultureInfo.GetCultureInfo(from.CurrentCulture); + Separators = from.Separators; + TrimChars = from.TrimChars; + Metadata = from.Metadata; + } + + public MetadataOptions(string[] separators, char[] trimChars, StringSplitOptions stringSplitOption, + NumberStyles numberStyle, CultureInfo currentCulture, IDictionary metadata) + { + Separators = separators; + TrimChars = trimChars; + StringSplitOption = stringSplitOption; + NumberStyle = numberStyle; + CurrentCulture = currentCulture; + Metadata = metadata; + } + + public string[] Separators { get; } + public char[] TrimChars { get; } + public StringSplitOptions StringSplitOption { get; } + public NumberStyles NumberStyle { get; } + public CultureInfo CurrentCulture { get; } + public IDictionary Metadata { get; set; } +} diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index e1897bc58..071bfbf95 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -32,16 +32,12 @@ public QoSOptions( TimeoutValue = timeoutValue; } - /// - /// How long the circuit should stay open before resetting in milliseconds. - /// - /// - /// If using Polly version 8 or above, this value must be 500 (0.5 sec) or greater. - /// - /// - /// An value (milliseconds). - /// - public int DurationOfBreak { get; } + /// How long the circuit should stay open before resetting in milliseconds. + /// If using Polly version 8 or above, this value must be 500 (0.5 sec) or greater. + /// An value (milliseconds). + public int DurationOfBreak { get; } = DefaultBreakDuration; + public const int LowBreakDuration = 500; // 0.5 seconds + public const int DefaultBreakDuration = 5_000; // 5 seconds /// /// How many times a circuit can fail before being set to open. diff --git a/src/Ocelot/Configuration/Route.cs b/src/Ocelot/Configuration/Route.cs index 8f9c0992f..12c57949c 100644 --- a/src/Ocelot/Configuration/Route.cs +++ b/src/Ocelot/Configuration/Route.cs @@ -10,7 +10,8 @@ public Route(List downstreamRoute, List upstreamHttpMethod, UpstreamPathTemplate upstreamTemplatePattern, string upstreamHost, - string aggregator) + string aggregator, + IDictionary upstreamHeaderTemplates) { UpstreamHost = upstreamHost; DownstreamRoute = downstreamRoute; @@ -18,8 +19,10 @@ public Route(List downstreamRoute, UpstreamHttpMethod = upstreamHttpMethod; UpstreamTemplatePattern = upstreamTemplatePattern; Aggregator = aggregator; + UpstreamHeaderTemplates = upstreamHeaderTemplates; } + public IDictionary UpstreamHeaderTemplates { get; } public UpstreamPathTemplate UpstreamTemplatePattern { get; } public List UpstreamHttpMethod { get; } public string UpstreamHost { get; } diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs index 7e74251e2..c8596b2d5 100644 --- a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs @@ -123,15 +123,15 @@ private static bool DoesNotContainRoutesWithSpecificRequestIdKeys(FileAggregateR return routesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey)); } - private static bool IsNotDuplicateIn(FileRoute route, - IEnumerable routes) + private static bool IsNotDuplicateIn(FileRoute route, IEnumerable routes) { var matchingRoutes = routes .Where(r => r.UpstreamPathTemplate == route.UpstreamPathTemplate - && r.UpstreamHost == route.UpstreamHost) - .ToList(); + && r.UpstreamHost == route.UpstreamHost + && AreTheSame(r.UpstreamHeaderTemplates, route.UpstreamHeaderTemplates)) + .ToArray(); - if (matchingRoutes.Count == 1) + if (matchingRoutes.Length == 1) { return true; } @@ -150,7 +150,11 @@ private static bool IsNotDuplicateIn(FileRoute route, } return true; - } + } + + private static bool AreTheSame(IDictionary upstreamHeaderTemplates, IDictionary otherHeaderTemplates) + => upstreamHeaderTemplates.Count == otherHeaderTemplates.Count && + upstreamHeaderTemplates.All(x => otherHeaderTemplates.ContainsKey(x.Key) && otherHeaderTemplates[x.Key] == x.Value); private static bool IsNotDuplicateIn(FileRoute route, IEnumerable aggregateRoutes) diff --git a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs index f383bf75c..900c53189 100644 --- a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs @@ -1,6 +1,7 @@ using FluentValidation; using Microsoft.AspNetCore.Authentication; using Ocelot.Configuration.File; +using Ocelot.Configuration.Creator; namespace Ocelot.Configuration.Validator { @@ -84,6 +85,11 @@ public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemePr { RuleFor(r => r.DownstreamHttpVersion).Matches("^[0-9]([.,][0-9]{1,1})?$"); }); + + When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersionPolicy), () => + { + RuleFor(r => r.DownstreamHttpVersionPolicy).Matches($@"^({VersionPolicies.RequestVersionExact}|{VersionPolicies.RequestVersionOrHigher}|{VersionPolicies.RequestVersionOrLower})$"); + }); } private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken) diff --git a/src/Ocelot/DependencyInjection/Features.cs b/src/Ocelot/DependencyInjection/Features.cs new file mode 100644 index 000000000..1ea558932 --- /dev/null +++ b/src/Ocelot/DependencyInjection/Features.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Cache; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.DownstreamRouteFinder.HeaderMatcher; +using Ocelot.RateLimiting; + +namespace Ocelot.DependencyInjection; + +public static class Features +{ + /// + /// Ocelot feature: Rate Limiting. + /// + /// + /// Read The Docs: Rate Limiting. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddRateLimiting(this IServiceCollection services) => services + .AddSingleton() + .AddSingleton(); + + /// + /// Ocelot feature: Request Caching. + /// + /// + /// Read The Docs: Caching. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddOcelotCache(this IServiceCollection services) => services + .AddSingleton, DefaultMemoryCache>() + .AddSingleton, DefaultMemoryCache>() + .AddSingleton() + .AddSingleton(); + + /// + /// Ocelot feature: Routing based on request header. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddHeaderRouting(this IServiceCollection services) => services + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + /// + /// Ocelot feature: Inject custom metadata and use it in delegating handlers. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddOcelotMetadata(this IServiceCollection services) => + services.AddSingleton(); +} diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index a72ec3cbf..217280e37 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -27,7 +27,7 @@ using Ocelot.Multiplexer; using Ocelot.PathManipulation; using Ocelot.QueryStrings; -using Ocelot.RateLimit; +using Ocelot.RateLimiting; using Ocelot.Request.Creator; using Ocelot.Request.Mapper; using Ocelot.Requester; @@ -54,8 +54,6 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services = services; Services.Configure(configurationRoot); - Services.TryAddSingleton, AspMemoryCache>(); - Services.TryAddSingleton, AspMemoryCache>(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -81,7 +79,6 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -109,14 +106,17 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.AddRateLimiting(); // Feature: Rate Limiting Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton, OcelotConfigurationMonitor>(); + + Services.AddOcelotCache(); + Services.AddOcelotMetadata(); Services.AddOcelotMessageInvokerPool(); // See this for why we register this as singleton: @@ -138,12 +138,16 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); // Add security Services.TryAddSingleton(); Services.TryAddSingleton(); + // Features + Services.AddHeaderRouting(); + // Add ASP.NET services var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly; MvcCoreBuilder = (customBuilder ?? AddDefaultAspNetServices) diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs index be4d5e32b..ef2590ae5 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs @@ -18,7 +18,8 @@ public DownstreamRouteCreator(IQoSOptionsCreator qoSOptionsCreator) _cache = new ConcurrentDictionary>(); } - public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost) + public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, + IInternalConfiguration configuration, string upstreamHost, IDictionary upstreamHeaders) { var serviceName = GetServiceName(upstreamUrlPath); @@ -69,7 +70,7 @@ public Response Get(string upstreamUrlPath, string upstre var route = new RouteBuilder() .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { upstreamHttpMethod }) - .WithUpstreamPathTemplate(upstreamPathTemplate) + .WithUpstreamPathTemplate(upstreamPathTemplate) .Build(); downstreamRouteHolder = new OkResponse(new DownstreamRouteHolder(new List(), route)); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 59cf1f7b7..fab94d4e1 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -1,21 +1,31 @@ using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Responses; +using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.Finder { public class DownstreamRouteFinder : IDownstreamRouteProvider { private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; - private readonly IPlaceholderNameAndValueFinder _placeholderNameAndValueFinder; + private readonly IPlaceholderNameAndValueFinder _pathPlaceholderFinder; + private readonly IHeadersToHeaderTemplatesMatcher _headerMatcher; + private readonly IHeaderPlaceholderNameAndValueFinder _headerPlaceholderFinder; - public DownstreamRouteFinder(IUrlPathToUrlTemplateMatcher urlMatcher, IPlaceholderNameAndValueFinder urlPathPlaceholderNameAndValueFinder) + public DownstreamRouteFinder( + IUrlPathToUrlTemplateMatcher urlMatcher, + IPlaceholderNameAndValueFinder pathPlaceholderFinder, + IHeadersToHeaderTemplatesMatcher headerMatcher, + IHeaderPlaceholderNameAndValueFinder headerPlaceholderFinder) { _urlMatcher = urlMatcher; - _placeholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; + _pathPlaceholderFinder = pathPlaceholderFinder; + _headerMatcher = headerMatcher; + _headerPlaceholderFinder = headerPlaceholderFinder; } - public Response Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, IInternalConfiguration configuration, string upstreamHost) + public Response Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, + IInternalConfiguration configuration, string upstreamHost, IDictionary upstreamHeaders) { var downstreamRoutes = new List(); @@ -25,20 +35,20 @@ public Response Get(string upstreamUrlPath, string upstre foreach (var route in applicableRoutes) { - var urlMatch = _urlMatcher.Match(upstreamUrlPath, upstreamQueryString, route.UpstreamTemplatePattern); + var urlMatch = _urlMatcher.Match(upstreamUrlPath, upstreamQueryString, route.UpstreamTemplatePattern); + var headersMatch = _headerMatcher.Match(upstreamHeaders, route.UpstreamHeaderTemplates); - if (urlMatch.Data.Match) + if (urlMatch.Data.Match && headersMatch) { - downstreamRoutes.Add(GetPlaceholderNamesAndValues(upstreamUrlPath, upstreamQueryString, route)); + downstreamRoutes.Add(GetPlaceholderNamesAndValues(upstreamUrlPath, upstreamQueryString, route, upstreamHeaders)); } - } - - if (downstreamRoutes.Any()) + } + + if (downstreamRoutes.Count != 0) { var notNullOption = downstreamRoutes.FirstOrDefault(x => !string.IsNullOrEmpty(x.Route.UpstreamHost)); - var nullOption = downstreamRoutes.FirstOrDefault(x => string.IsNullOrEmpty(x.Route.UpstreamHost)); - - return notNullOption != null ? new OkResponse(notNullOption) : new OkResponse(nullOption); + var nullOption = downstreamRoutes.FirstOrDefault(x => string.IsNullOrEmpty(x.Route.UpstreamHost)); + return new OkResponse(notNullOption ?? nullOption); } return new ErrorResponse(new UnableToFindDownstreamRouteError(upstreamUrlPath, httpMethod)); @@ -50,11 +60,15 @@ private static bool RouteIsApplicableToThisRequest(Route route, string httpMetho (string.IsNullOrEmpty(route.UpstreamHost) || route.UpstreamHost == upstreamHost); } - private DownstreamRouteHolder GetPlaceholderNamesAndValues(string path, string query, Route route) + private DownstreamRouteHolder GetPlaceholderNamesAndValues(string path, string query, Route route, IDictionary upstreamHeaders) { - var templatePlaceholderNameAndValues = _placeholderNameAndValueFinder.Find(path, query, route.UpstreamTemplatePattern.OriginalValue); + var templatePlaceholderNameAndValues = _pathPlaceholderFinder + .Find(path, query, route.UpstreamTemplatePattern.OriginalValue) + .Data; + var headerPlaceholders = _headerPlaceholderFinder.Find(upstreamHeaders, route.UpstreamHeaderTemplates); + templatePlaceholderNameAndValues.AddRange(headerPlaceholders); - return new DownstreamRouteHolder(templatePlaceholderNameAndValues.Data, route); + return new DownstreamRouteHolder(templatePlaceholderNameAndValues, route); } } } diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs index ed2a657ef..c30ba31bc 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs @@ -1,10 +1,10 @@ using Ocelot.Configuration; using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.Finder; -namespace Ocelot.DownstreamRouteFinder.Finder +public interface IDownstreamRouteProvider { - public interface IDownstreamRouteProvider - { - Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost); - } + Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, + IInternalConfiguration configuration, string upstreamHost, IDictionary upstreamHeaders); } diff --git a/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinder.cs new file mode 100644 index 000000000..56e55b2f4 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinder.cs @@ -0,0 +1,24 @@ +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Values; + +namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; + +public class HeaderPlaceholderNameAndValueFinder : IHeaderPlaceholderNameAndValueFinder +{ + public IList Find(IDictionary upstreamHeaders, IDictionary templateHeaders) + { + var result = new List(); + foreach (var templateHeader in templateHeaders) + { + var upstreamHeader = upstreamHeaders[templateHeader.Key]; + var matches = templateHeader.Value.Pattern.Matches(upstreamHeader); + var placeholders = matches + .SelectMany(g => g.Groups as IEnumerable) + .Where(g => g.Name != "0") + .Select(g => new PlaceholderNameAndValue(string.Concat('{', g.Name, '}'), g.Value)); + result.AddRange(placeholders); + } + + return result; + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcher.cs b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcher.cs new file mode 100644 index 000000000..42be8dc7f --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcher.cs @@ -0,0 +1,11 @@ +using Ocelot.Values; + +namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; + +public class HeadersToHeaderTemplatesMatcher : IHeadersToHeaderTemplatesMatcher +{ + public bool Match(IDictionary upstreamHeaders, IDictionary routeHeaders) => + routeHeaders == null || + upstreamHeaders != null + && routeHeaders.All(h => upstreamHeaders.ContainsKey(h.Key) && routeHeaders[h.Key].Pattern.IsMatch(upstreamHeaders[h.Key])); +} diff --git a/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeaderPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeaderPlaceholderNameAndValueFinder.cs new file mode 100644 index 000000000..6f641d278 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeaderPlaceholderNameAndValueFinder.cs @@ -0,0 +1,12 @@ +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Values; + +namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; + +/// +/// Ocelot feature: Routing based on request header. +/// +public interface IHeaderPlaceholderNameAndValueFinder +{ + IList Find(IDictionary upstreamHeaders, IDictionary templateHeaders); +} diff --git a/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeadersToHeaderTemplatesMatcher.cs b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeadersToHeaderTemplatesMatcher.cs new file mode 100644 index 000000000..37dcea32a --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeadersToHeaderTemplatesMatcher.cs @@ -0,0 +1,11 @@ +using Ocelot.Values; + +namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; + +/// +/// Ocelot feature: Routing based on request header. +/// +public interface IHeadersToHeaderTemplatesMatcher +{ + bool Match(IDictionary upstreamHeaders, IDictionary routeHeaders); +} diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index 63c21b76c..38ab1bd36 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -24,26 +24,22 @@ IDownstreamRouteProviderFactory downstreamRouteFinder public async Task Invoke(HttpContext httpContext) { var upstreamUrlPath = httpContext.Request.Path.ToString(); - var upstreamQueryString = httpContext.Request.QueryString.ToString(); - - var hostHeader = httpContext.Request.Headers["Host"].ToString(); + var internalConfiguration = httpContext.Items.IInternalConfiguration(); + var hostHeader = httpContext.Request.Headers.Host.ToString(); var upstreamHost = hostHeader.Contains(':') ? hostHeader.Split(':')[0] : hostHeader; + var upstreamHeaders = httpContext.Request.Headers + .ToDictionary(h => h.Key, h => string.Join(';', h.Value)); - Logger.LogDebug(() => $"Upstream url path is {upstreamUrlPath}"); - - var internalConfiguration = httpContext.Items.IInternalConfiguration(); + Logger.LogDebug(() => $"Upstream URL path is '{upstreamUrlPath}'."); var provider = _factory.Get(internalConfiguration); - - var response = provider.Get(upstreamUrlPath, upstreamQueryString, httpContext.Request.Method, internalConfiguration, upstreamHost); - + var response = provider.Get(upstreamUrlPath, upstreamQueryString, httpContext.Request.Method, internalConfiguration, upstreamHost, upstreamHeaders); if (response.IsError) { Logger.LogWarning(() => $"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {response.Errors.ToErrorString()}"); - httpContext.Items.UpsertErrors(response.Errors); return; } @@ -52,7 +48,6 @@ public async Task Invoke(HttpContext httpContext) // why set both of these on HttpContext httpContext.Items.UpsertTemplatePlaceholderNameAndValues(response.Data.TemplatePlaceholderNameAndValues); - httpContext.Items.UpsertDownstreamRoute(response.Data); await _next.Invoke(httpContext); diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs index ccaf099cc..7f9163fa3 100644 --- a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs @@ -1,14 +1,15 @@ -namespace Ocelot.DownstreamRouteFinder.UrlMatcher +namespace Ocelot.DownstreamRouteFinder.UrlMatcher; + +public class PlaceholderNameAndValue { - public class PlaceholderNameAndValue + public PlaceholderNameAndValue(string name, string value) { - public PlaceholderNameAndValue(string name, string value) - { - Name = name; - Value = value; - } - - public string Name { get; } - public string Value { get; } + Name = name; + Value = value; } + + public string Name { get; } + public string Value { get; } + + public override string ToString() => $"[{{{Name}}}={Value}]"; } diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index a92f6a470..dc7fb312e 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -99,6 +99,13 @@ private static string MergeQueryStringsWithoutDuplicateValues(string queryString var queries = HttpUtility.ParseQueryString(queryString); var newQueries = HttpUtility.ParseQueryString(newQueryString); + // Remove old replaced query parameters + var placeholderNames = new HashSet(placeholders.Select(p => p.Name.Trim(OpeningBrace, ClosingBrace))); + foreach (var queryKey in queries.AllKeys.Where(placeholderNames.Contains)) + { + queries.Remove(queryKey); + } + var parameters = newQueries.AllKeys .Where(key => !string.IsNullOrEmpty(key)) .ToDictionary(key => key, key => newQueries[key]); @@ -107,16 +114,11 @@ private static string MergeQueryStringsWithoutDuplicateValues(string queryString .Where(key => !string.IsNullOrEmpty(key) && !parameters.ContainsKey(key)) .All(key => parameters.TryAdd(key, queries[key])); - // Remove old replaced query parameters - foreach (var placeholder in placeholders) - { - parameters.Remove(placeholder.Name.Trim(OpeningBrace, ClosingBrace)); - } - - var orderedParams = parameters.OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}"); - return QuestionMark + string.Join(Ampersand, orderedParams); + return QuestionMark + string.Join(Ampersand, parameters.Select(MapQueryParameter)); } + private static string MapQueryParameter(KeyValuePair pair) => $"{pair.Key}={pair.Value}"; + private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamRequest downstreamRequest, List templatePlaceholderNameAndValues) { foreach (var nAndV in templatePlaceholderNameAndValues) diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs index 4a3dc798f..dfa6279e6 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs @@ -1,4 +1,5 @@ using Ocelot.Configuration; +using Ocelot.Errors; using Ocelot.Responses; namespace Ocelot.LoadBalancer.LoadBalancers @@ -33,10 +34,10 @@ public Response Get(DownstreamRoute route, ServiceProviderConfigu } catch (Exception ex) { - return new ErrorResponse( - [ + return new ErrorResponse(new List() + { new UnableToFindLoadBalancerError($"Unable to find load balancer for '{route.LoadBalancerKey}'. Exception: {ex};"), - ]); + }); } } diff --git a/src/Ocelot/Metadata/DownstreamRouteExtensions.cs b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs new file mode 100644 index 000000000..ca3ba25e6 --- /dev/null +++ b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs @@ -0,0 +1,165 @@ +using Ocelot.Configuration; +using System.Globalization; +using System.Reflection; +using System.Text.Json; + +namespace Ocelot.Metadata; + +public static class DownstreamRouteExtensions +{ + /// + /// The known truthy values. + /// + private static readonly HashSet TruthyValues = + new(StringComparer.OrdinalIgnoreCase) + { + "true", + "yes", + "on", + "ok", + "enable", + "enabled", + "1", + }; + + /// + /// The known falsy values. + /// + private static readonly HashSet FalsyValues = + new(StringComparer.OrdinalIgnoreCase) + { + "false", + "no", + "off", + "disable", + "disabled", + "0", + }; + + /// + /// The known numeric types. + /// + private static readonly HashSet NumericTypes = new() + { + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + }; + + /// + /// Extension method to get metadata from a downstream route. + /// + /// The metadata target type. + /// The current downstream route. + /// The metadata key in downstream route Metadata dictionary. + /// The fallback value if no value found. + /// Custom json serializer options if needed. + /// The parsed metadata value. + public static T GetMetadata(this DownstreamRoute downstreamRoute, string key, T defaultValue = default, + JsonSerializerOptions jsonSerializerOptions = null) + { + var metadata = downstreamRoute?.MetadataOptions.Metadata; + + if (metadata == null || !metadata.TryGetValue(key, out var metadataValue) || metadataValue == null) + { + return defaultValue; + } + + return (T)ConvertTo(typeof(T), metadataValue, downstreamRoute.MetadataOptions, + jsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web)); + } + + /// + /// Converting a string value to the target type. + /// Some custom conversion has been for the following types: + /// , , , numeric types; + /// otherwise trying to deserialize the value using the JsonSerializer. + /// + /// The target type. + /// The string value. + /// The metadata options, it includes the global configuration. + /// If needed, some custom json serializer options. + /// The converted string. + private static object ConvertTo(Type targetType, string value, MetadataOptions metadataOptions, + JsonSerializerOptions jsonSerializerOptions) + { + if (targetType == typeof(string)) + { + return value; + } + + if (targetType == typeof(bool)) + { + return TruthyValues.Contains(value.Trim()); + } + + if (targetType == typeof(bool?)) + { + if (TruthyValues.Contains(value.Trim())) + { + return true; + } + + if (FalsyValues.Contains(value.Trim())) + { + return false; + } + + return null; + } + + if (targetType == typeof(string[])) + { + if (value == null) + { + return Array.Empty(); + } + + return value.Split(metadataOptions.Separators, metadataOptions.StringSplitOption) + .Select(s => s.Trim(metadataOptions.TrimChars)) + .Where(s => !string.IsNullOrEmpty(s)) + .ToArray(); + } + + return NumericTypes.Contains(targetType) + ? ConvertToNumericType(value, targetType, metadataOptions.CurrentCulture, metadataOptions.NumberStyle) + : JsonSerializer.Deserialize(value, targetType, jsonSerializerOptions); + } + + /// + /// Converting string to the known numeric types. + /// + /// The number as string. + /// The target numeric type. + /// The current format provider. + /// The current number style configuration. + /// The parsed string as object of type targetType. + /// Exception thrown if the conversion for the type target type can't be found. + private static object ConvertToNumericType(string value, Type targetType, IFormatProvider provider, + NumberStyles numberStyle) + { + return targetType switch + { + { } t when t == typeof(byte) => byte.Parse(value, numberStyle, provider), + { } t when t == typeof(sbyte) => sbyte.Parse(value, numberStyle, provider), + { } t when t == typeof(short) => short.Parse(value, numberStyle, provider), + { } t when t == typeof(ushort) => ushort.Parse(value, numberStyle, provider), + { } t when t == typeof(int) => int.Parse(value, numberStyle, provider), + { } t when t == typeof(uint) => uint.Parse(value, numberStyle, provider), + { } t when t == typeof(long) => long.Parse(value, numberStyle, provider), + { } t when t == typeof(ulong) => ulong.Parse(value, numberStyle, provider), + { } t when t == typeof(float) => float.Parse(value, numberStyle, provider), + { } t when t == typeof(double) => double.Parse(value, numberStyle, provider), + { } t when t == typeof(decimal) => decimal.Parse(value, numberStyle, provider), + _ => throw new NotImplementedException($"No conversion available for the type: {targetType.Name}"), + }; + } +} diff --git a/src/Ocelot/Middleware/HttpItemsExtensions.cs b/src/Ocelot/Middleware/HttpItemsExtensions.cs index d8d82ef9d..df97e0a19 100644 --- a/src/Ocelot/Middleware/HttpItemsExtensions.cs +++ b/src/Ocelot/Middleware/HttpItemsExtensions.cs @@ -56,7 +56,7 @@ public static IInternalConfiguration IInternalConfiguration(this IDictionary Errors(this IDictionary input) { var errors = input.Get>("Errors"); - return errors ?? []; + return errors ?? new(); } public static DownstreamRouteFinder.DownstreamRouteHolder diff --git a/src/Ocelot/Middleware/OcelotPipelineExtensions.cs b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs index 01ec573fb..16f4a5cff 100644 --- a/src/Ocelot/Middleware/OcelotPipelineExtensions.cs +++ b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs @@ -12,7 +12,7 @@ using Ocelot.LoadBalancer.Middleware; using Ocelot.Multiplexer; using Ocelot.QueryStrings.Middleware; -using Ocelot.RateLimit.Middleware; +using Ocelot.RateLimiting.Middleware; using Ocelot.Request.Middleware; using Ocelot.Requester.Middleware; using Ocelot.RequestId.Middleware; diff --git a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs index 731909006..43a98fcd3 100644 --- a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs +++ b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs @@ -19,8 +19,7 @@ public class MultiplexingMiddleware : OcelotMiddleware public MultiplexingMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, - IResponseAggregatorFactory factory - ) + IResponseAggregatorFactory factory) : base(loggerFactory.CreateLogger()) { _factory = factory; @@ -104,7 +103,7 @@ private async Task ProcessRoutesAsync(HttpContext context, Route route) .Select(downstreamRoute => ProcessRouteAsync(context, downstreamRoute)) .ToArray(); var contexts = await Task.WhenAll(tasks); - await MapAsync(context, route, [.. contexts]); + await MapAsync(context, route, new(contexts)); } /// @@ -184,7 +183,7 @@ private IEnumerable> ProcessRouteWithComplexAggregation(Aggreg /// The cloned Http context. private async Task ProcessRouteAsync(HttpContext sourceContext, DownstreamRoute route, List placeholders = null) { - var newHttpContext = CreateThreadContext(sourceContext); + var newHttpContext = await CreateThreadContextAsync(sourceContext); CopyItemsToNewContext(newHttpContext, sourceContext, placeholders); newHttpContext.Items.UpsertDownstreamRoute(route); @@ -208,14 +207,15 @@ private static void CopyItemsToNewContext(HttpContext target, HttpContext source /// /// The base http context. /// The cloned context. - private static HttpContext CreateThreadContext(HttpContext source) + protected virtual async Task CreateThreadContextAsync(HttpContext source) { - var from = source.Request; + var from = source.Request; + var bodyStream = await CloneRequestBodyAsync(from, source.RequestAborted); var target = new DefaultHttpContext { Request = { - Body = from.Body, // TODO Consider stream cloning for multiple reads + Body = bodyStream, ContentLength = from.ContentLength, ContentType = from.ContentType, Host = from.Host, @@ -237,12 +237,13 @@ private static HttpContext CreateThreadContext(HttpContext source) RequestAborted = source.RequestAborted, User = source.User, }; - foreach (var header in from.Headers) { target.Request.Headers[header.Key] = header.Value.ToArray(); - } - + } + + // Once the downstream request is completed and the downstream response has been read, the downstream response object can dispose of the body's Stream object + target.Response.RegisterForDisposeAsync(bodyStream); // manage Stream lifetime by HttpResponse object return target; } @@ -255,5 +256,29 @@ protected virtual Task MapAsync(HttpContext httpContext, Route route, List CloneRequestBodyAsync(HttpRequest request, CancellationToken aborted) + { + request.EnableBuffering(); + if (request.Body.Position != 0) + { + Logger.LogWarning("Ocelot does not support body copy without stream in initial position 0"); + return request.Body; + } + + var targetBuffer = new MemoryStream(); + if (request.ContentLength is not null) + { + await request.Body.CopyToAsync(targetBuffer, (int)request.ContentLength, aborted); + targetBuffer.Position = 0; + request.Body.Position = 0; + } + else + { + Logger.LogWarning("Aggregation does not support body copy without Content-Length header!"); + } + + return targetBuffer; + } } diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 19f0a5bdc..b876ca4b7 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -4,7 +4,7 @@ disable disable true - Ocelot is an API Gateway. The project is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. In particular I want easy integration with IdentityServer reference and bearer tokens. reference tokens. Ocelot is a bunch of middlewares in a specific order. Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is stored in a per request scoped repository and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features. + Ocelot is an API gateway based on .NET stack. Ocelot 0.0.0-dev Ocelot @@ -12,6 +12,7 @@ API Gateway;.NET core https://github.com/ThreeMammals/Ocelot https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png + README.md win-x64;osx-x64 false false @@ -55,4 +56,7 @@ + + + diff --git a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs deleted file mode 100644 index 14ebc0594..000000000 --- a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; - -namespace Ocelot.RateLimit -{ - public class ClientRateLimitProcessor - { - private readonly RateLimitCore _core; - - public ClientRateLimitProcessor(IRateLimitCounterHandler counterHandler) - { - _core = new RateLimitCore(counterHandler); - } - - public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - return _core.ProcessRequest(requestIdentity, option); - } - - public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) - { - return _core.RetryAfterFrom(timestamp, rule); - } - - public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - return _core.GetRateLimitHeaders(context, requestIdentity, option); - } - - public TimeSpan ConvertToTimeSpan(string timeSpan) - { - return _core.ConvertToTimeSpan(timeSpan); - } - } -} diff --git a/src/Ocelot/RateLimit/ClientRequestIdentity.cs b/src/Ocelot/RateLimit/ClientRequestIdentity.cs deleted file mode 100644 index b67b7c5a9..000000000 --- a/src/Ocelot/RateLimit/ClientRequestIdentity.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Ocelot.RateLimit -{ - public class ClientRequestIdentity - { - public ClientRequestIdentity(string clientId, string path, string httpverb) - { - ClientId = clientId; - Path = path; - HttpVerb = httpverb; - } - - public string ClientId { get; } - - public string Path { get; } - - public string HttpVerb { get; } - } -} diff --git a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHandler.cs deleted file mode 100644 index c98e256ad..000000000 --- a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.Caching.Distributed; -using Newtonsoft.Json; - -namespace Ocelot.RateLimit -{ - public class DistributedCacheRateLimitCounterHandler : IRateLimitCounterHandler - { - private readonly IDistributedCache _memoryCache; - - public DistributedCacheRateLimitCounterHandler(IDistributedCache memoryCache) - { - _memoryCache = memoryCache; - } - - public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) - { - _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); - } - - public bool Exists(string id) - { - var stored = _memoryCache.GetString(id); - return !string.IsNullOrEmpty(stored); - } - - public RateLimitCounter? Get(string id) - { - var stored = _memoryCache.GetString(id); - if (!string.IsNullOrEmpty(stored)) - { - return JsonConvert.DeserializeObject(stored); - } - - return null; - } - - public void Remove(string id) - { - _memoryCache.Remove(id); - } - } -} diff --git a/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs deleted file mode 100644 index c17d04f7c..000000000 --- a/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Ocelot.RateLimit -{ - public interface IRateLimitCounterHandler - { - bool Exists(string id); - - RateLimitCounter? Get(string id); - - void Remove(string id); - - void Set(string id, RateLimitCounter counter, TimeSpan expirationTime); - } -} diff --git a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs deleted file mode 100644 index 1a030d511..000000000 --- a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; - -namespace Ocelot.RateLimit -{ - public class MemoryCacheRateLimitCounterHandler : IRateLimitCounterHandler - { - private readonly IMemoryCache _memoryCache; - - public MemoryCacheRateLimitCounterHandler(IMemoryCache memoryCache) - { - _memoryCache = memoryCache; - } - - public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) - { - _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); - } - - public bool Exists(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter); - - public RateLimitCounter? Get(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter) ? counter : null; - - public void Remove(string id) - { - _memoryCache.Remove(id); - } - } -} diff --git a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs deleted file mode 100644 index 91609c67f..000000000 --- a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Builder; - -namespace Ocelot.RateLimit.Middleware -{ - public static class RateLimitMiddlewareExtensions - { - public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } -} diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs deleted file mode 100644 index dddf8a772..000000000 --- a/src/Ocelot/RateLimit/RateLimitCore.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using System.Globalization; -using System.Security.Cryptography; - -namespace Ocelot.RateLimit -{ - public class RateLimitCore - { - private readonly IRateLimitCounterHandler _counterHandler; - private static readonly object ProcessLocker = new(); - - public RateLimitCore(IRateLimitCounterHandler counterStore) - { - _counterHandler = counterStore; - } - - public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var counter = new RateLimitCounter(DateTime.UtcNow, 1); - var rule = option.RateLimitRule; - - var counterId = ComputeCounterKey(requestIdentity, option); - - // serial reads and writes - lock (ProcessLocker) - { - var entry = _counterHandler.Get(counterId); - if (entry.HasValue) - { - // entry has not expired - if (entry.Value.Timestamp + TimeSpan.FromSeconds(rule.PeriodTimespan) >= DateTime.UtcNow) - { - // increment request count - var totalRequests = entry.Value.TotalRequests + 1; - - // deep copy - counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests); - } - } - } - - if (counter.TotalRequests > rule.Limit) - { - var retryAfter = RetryAfterFrom(counter.Timestamp, rule); - if (retryAfter > 0) - { - var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan); - _counterHandler.Set(counterId, counter, expirationTime); - } - else - { - _counterHandler.Remove(counterId); - } - } - else - { - var expirationTime = ConvertToTimeSpan(rule.Period); - _counterHandler.Set(counterId, counter, expirationTime); - } - - return counter; - } - - public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime) - { - var counterId = ComputeCounterKey(requestIdentity, option); - var rule = option.RateLimitRule; - - // stores: id (string) - timestamp (datetime) - total_requests (long) - _counterHandler.Set(counterId, counter, expirationTime); - } - - public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var rule = option.RateLimitRule; - RateLimitHeaders headers; - var counterId = ComputeCounterKey(requestIdentity, option); - var entry = _counterHandler.Get(counterId); - if (entry.HasValue) - { - headers = new RateLimitHeaders(context, rule.Period, - (rule.Limit - entry.Value.TotalRequests).ToString(), - (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo) - ); - } - else - { - headers = new RateLimitHeaders(context, - rule.Period, - rule.Limit.ToString(), - (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); - } - - return headers; - } - - public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}"; - - var idBytes = Encoding.UTF8.GetBytes(key); - - byte[] hashBytes; - - using (var algorithm = SHA1.Create()) - { - hashBytes = algorithm.ComputeHash(idBytes); - } - - return BitConverter.ToString(hashBytes).Replace("-", string.Empty); - } - - public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) - { - var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds); - var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds); - retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1; - return retryAfter; - } - - public TimeSpan ConvertToTimeSpan(string timeSpan) - { - var l = timeSpan.Length - 1; - var value = timeSpan.Substring(0, l); - var type = timeSpan.Substring(l, 1); - - switch (type) - { - case "d": - return TimeSpan.FromDays(double.Parse(value)); - - case "h": - return TimeSpan.FromHours(double.Parse(value)); - - case "m": - return TimeSpan.FromMinutes(double.Parse(value)); - - case "s": - return TimeSpan.FromSeconds(double.Parse(value)); - - default: - throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}"); - } - } - } -} diff --git a/src/Ocelot/RateLimit/RateLimitCounter.cs b/src/Ocelot/RateLimit/RateLimitCounter.cs deleted file mode 100644 index 4e869d440..000000000 --- a/src/Ocelot/RateLimit/RateLimitCounter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace Ocelot.RateLimit -{ - /// - /// Stores the initial access time and the numbers of calls made from that point. - /// - public struct RateLimitCounter - { - [JsonConstructor] - public RateLimitCounter(DateTime timestamp, long totalRequests) - { - Timestamp = timestamp; - TotalRequests = totalRequests; - } - - public DateTime Timestamp { get; } - - public long TotalRequests { get; } - } -} diff --git a/src/Ocelot/RateLimit/RateLimitHeaders.cs b/src/Ocelot/RateLimit/RateLimitHeaders.cs deleted file mode 100644 index 67d7596ce..000000000 --- a/src/Ocelot/RateLimit/RateLimitHeaders.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace Ocelot.RateLimit -{ - public class RateLimitHeaders - { - public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset) - { - Context = context; - Limit = limit; - Remaining = remaining; - Reset = reset; - } - - public HttpContext Context { get; } - - public string Limit { get; } - - public string Remaining { get; } - - public string Reset { get; } - } -} diff --git a/src/Ocelot/RateLimiting/ClientRequestIdentity.cs b/src/Ocelot/RateLimiting/ClientRequestIdentity.cs new file mode 100644 index 000000000..b73fcbbbb --- /dev/null +++ b/src/Ocelot/RateLimiting/ClientRequestIdentity.cs @@ -0,0 +1,15 @@ +namespace Ocelot.RateLimiting; + +public class ClientRequestIdentity +{ + public ClientRequestIdentity(string clientId, string path, string httpverb) + { + ClientId = clientId; + Path = path; + HttpVerb = httpverb; + } + + public string ClientId { get; } + public string Path { get; } + public string HttpVerb { get; } +} diff --git a/src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs b/src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs new file mode 100644 index 000000000..b7fb79de3 --- /dev/null +++ b/src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Caching.Distributed; +using Newtonsoft.Json; + +namespace Ocelot.RateLimiting; + +/// +/// Custom storage based on a distributed cache of a remote/local services. +/// +/// +/// See the interface docs for more details. +/// +public class DistributedCacheRateLimitStorage : IRateLimitStorage +{ + private readonly IDistributedCache _memoryCache; + + public DistributedCacheRateLimitStorage(IDistributedCache memoryCache) => _memoryCache = memoryCache; + + public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) + => _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); + + public bool Exists(string id) => !string.IsNullOrEmpty(_memoryCache.GetString(id)); + + public RateLimitCounter? Get(string id) + { + var stored = _memoryCache.GetString(id); + return !string.IsNullOrEmpty(stored) + ? JsonConvert.DeserializeObject(stored) + : null; + } + + public void Remove(string id) => _memoryCache.Remove(id); +} diff --git a/src/Ocelot/RateLimiting/IRateLimitStorage.cs b/src/Ocelot/RateLimiting/IRateLimitStorage.cs new file mode 100644 index 000000000..1044998b1 --- /dev/null +++ b/src/Ocelot/RateLimiting/IRateLimitStorage.cs @@ -0,0 +1,16 @@ +namespace Ocelot.RateLimiting; + +/// +/// Defines a storage for keeping of rate limiting data. +/// +/// Concrete classes should be based on solutions with excellent performance, such as in-memory solutions. +public interface IRateLimitStorage +{ + bool Exists(string id); + + RateLimitCounter? Get(string id); + + void Remove(string id); + + void Set(string id, RateLimitCounter counter, TimeSpan expirationTime); +} diff --git a/src/Ocelot/RateLimiting/IRateLimiting.cs b/src/Ocelot/RateLimiting/IRateLimiting.cs new file mode 100644 index 000000000..684d2f70e --- /dev/null +++ b/src/Ocelot/RateLimiting/IRateLimiting.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; + +namespace Ocelot.RateLimiting; + +/// +/// Defines basic Rate Limiting functionality. +/// +public interface IRateLimiting +{ + /// Retrieves the key for the attached storage. + /// See the interface. + /// The current representation of the request. + /// The options of rate limiting. + /// A value of the key. + string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options); + + /// + /// Gets required information to create wanted headers in upper contexts (middleware, etc). + /// + /// The current context. + /// The current representation of the request. + /// The options of rate limiting. + /// A value. + RateLimitHeaders GetHeaders(HttpContext context, ClientRequestIdentity identity, RateLimitOptions options); + + /// + /// Main entry point to process the current request and apply the limiting rule. + /// + /// Warning! The method performs the storage operations which should be thread safe. + /// The representation of current request. + /// The current rate limiting options. + /// A value. + RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options); + + /// + /// Counts requests based on the current counter state and taking into account the limiting rule. + /// + /// Old counter with starting moment inside. + /// The limiting rule. + /// A value. + RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule); + + /// + /// Gets the seconds to wait for the next retry by starting moment and the rule. + /// + /// The method must be called after the counting by the method is completed; otherwise it doesn't make sense. + /// The counter with starting moment inside. + /// The limiting rule. + /// A value in seconds. + double RetryAfter(RateLimitCounter counter, RateLimitRule rule); + + /// + /// Converts to time span from a string, such as "1s", "1m", "1h", "1d". + /// + /// The string value with dimentions: '1s', '1m', '1h', '1d'. + /// A value. + TimeSpan ToTimespan(string timespan); +} diff --git a/src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs b/src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs new file mode 100644 index 000000000..7451dac97 --- /dev/null +++ b/src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace Ocelot.RateLimiting; + +/// +/// Default storage based on the memory cache of the local web server instance. +/// +/// +/// See the interface docs for more details. +/// +public class MemoryCacheRateLimitStorage : IRateLimitStorage +{ + private readonly IMemoryCache _memoryCache; + + public MemoryCacheRateLimitStorage(IMemoryCache memoryCache) => _memoryCache = memoryCache; + + public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) + => _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); + + public bool Exists(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter); + + public RateLimitCounter? Get(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter) ? counter : null; + + public void Remove(string id) => _memoryCache.Remove(id); +} diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs similarity index 83% rename from src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs rename to src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs index 47571046f..b407733ae 100644 --- a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs @@ -2,21 +2,23 @@ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Middleware; +using System.Globalization; -namespace Ocelot.RateLimit.Middleware +namespace Ocelot.RateLimiting.Middleware { - public class ClientRateLimitMiddleware : OcelotMiddleware + public class RateLimitingMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; - private readonly ClientRateLimitProcessor _processor; + private readonly IRateLimiting _limiter; - public ClientRateLimitMiddleware(RequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IRateLimitCounterHandler counterHandler) - : base(loggerFactory.CreateLogger()) + public RateLimitingMiddleware( + RequestDelegate next, + IOcelotLoggerFactory factory, + IRateLimiting limiter) + : base(factory.CreateLogger()) { _next = next; - _processor = new ClientRateLimitProcessor(counterHandler); + _limiter = limiter; } public async Task Invoke(HttpContext httpContext) @@ -48,26 +50,20 @@ public async Task Invoke(HttpContext httpContext) if (rule.Limit > 0) { // increment counter - var counter = _processor.ProcessRequest(identity, options); + var counter = _limiter.ProcessRequest(identity, options); // check if limit is reached if (counter.TotalRequests > rule.Limit) { - //compute retry after value - var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule); - - // log blocked request - LogBlockedRequest(httpContext, identity, counter, rule, downstreamRoute); - - var retrystring = retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture); + var retryAfter = _limiter.RetryAfter(counter, rule); // compute retry after value based on counter state + LogBlockedRequest(httpContext, identity, counter, rule, downstreamRoute); // log blocked request virtually // break execution - var ds = ReturnQuotaExceededResponse(httpContext, options, retrystring); + var ds = ReturnQuotaExceededResponse(httpContext, options, retryAfter.ToString(CultureInfo.InvariantCulture)); httpContext.Items.UpsertDownstreamResponse(ds); // Set Error httpContext.Items.SetError(new QuotaExceededError(GetResponseMessage(options), options.HttpStatusCode)); - return; } } @@ -75,7 +71,7 @@ public async Task Invoke(HttpContext httpContext) //set X-Rate-Limit headers for the longest period if (!options.DisableRateLimitHeaders) { - var headers = _processor.GetRateLimitHeaders(httpContext, identity, options); + var headers = _limiter.GetHeaders(httpContext, identity, options); httpContext.Response.OnStarting(SetRateLimitHeaders, state: headers); } @@ -123,7 +119,7 @@ public virtual DownstreamResponse ReturnQuotaExceededResponse(HttpContext httpCo if (!option.DisableRateLimitHeaders) { - http.Headers.TryAddWithoutValidation("Retry-After", retryAfter); + http.Headers.TryAddWithoutValidation("Retry-After", retryAfter); // in seconds, not date string } return new DownstreamResponse(http); diff --git a/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs new file mode 100644 index 000000000..68268cb40 --- /dev/null +++ b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.RateLimiting.Middleware; + +public static class RateLimitingMiddlewareExtensions +{ + public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} diff --git a/src/Ocelot/RateLimit/QuotaExceededError.cs b/src/Ocelot/RateLimiting/QuotaExceededError.cs similarity index 89% rename from src/Ocelot/RateLimit/QuotaExceededError.cs rename to src/Ocelot/RateLimiting/QuotaExceededError.cs index 9c98dc5a6..a46cb4c78 100644 --- a/src/Ocelot/RateLimit/QuotaExceededError.cs +++ b/src/Ocelot/RateLimiting/QuotaExceededError.cs @@ -1,6 +1,6 @@ using Ocelot.Errors; -namespace Ocelot.RateLimit +namespace Ocelot.RateLimiting { public class QuotaExceededError : Error { diff --git a/src/Ocelot/RateLimiting/RateLimitCounter.cs b/src/Ocelot/RateLimiting/RateLimitCounter.cs new file mode 100644 index 000000000..2507a0433 --- /dev/null +++ b/src/Ocelot/RateLimiting/RateLimitCounter.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; + +namespace Ocelot.RateLimiting; + +/// +/// Stores the initial access time and the numbers of calls made from that point. +/// +public struct RateLimitCounter +{ + [JsonConstructor] + public RateLimitCounter(DateTime startedAt, DateTime? exceededAt, long totalRequests) + { + StartedAt = startedAt; + ExceededAt = exceededAt; + TotalRequests = totalRequests; + } + + /// The moment when the counting was started. + /// A value of the moment. + public DateTime StartedAt { get; } + + /// The moment when the limit was exceeded. + /// A value of the moment. + public DateTime? ExceededAt { get; } + + /// Total number of requests counted. + /// A value of total number. + public long TotalRequests { get; set; } +} diff --git a/src/Ocelot/RateLimiting/RateLimitHeaders.cs b/src/Ocelot/RateLimiting/RateLimitHeaders.cs new file mode 100644 index 000000000..860e0d6bb --- /dev/null +++ b/src/Ocelot/RateLimiting/RateLimitHeaders.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Http; + +namespace Ocelot.RateLimiting; + +public class RateLimitHeaders +{ + public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset) + { + Context = context; + Limit = limit; + Remaining = remaining; + Reset = reset; + } + + public HttpContext Context { get; } + public string Limit { get; } + public string Remaining { get; } + public string Reset { get; } +} diff --git a/src/Ocelot/RateLimiting/RateLimiting.cs b/src/Ocelot/RateLimiting/RateLimiting.cs new file mode 100644 index 000000000..9edf4a310 --- /dev/null +++ b/src/Ocelot/RateLimiting/RateLimiting.cs @@ -0,0 +1,194 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using System.Globalization; +using System.Security.Cryptography; + +namespace Ocelot.RateLimiting; + +public class RateLimiting : IRateLimiting +{ + private readonly IRateLimitStorage _storage; + private static readonly object ProcessLocker = new(); + + public RateLimiting(IRateLimitStorage storage) + { + _storage = storage; + } + + /// + /// Main entry point to process the current request and apply the limiting rule. + /// + /// Warning! The method performs the storage operations which MUST BE thread safe. + /// The representation of current request. + /// The current rate limiting options. + /// A value. + public virtual RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options) + { + RateLimitCounter counter; + var rule = options.RateLimitRule; + var counterId = GetStorageKey(identity, options); + + // Serial reads/writes from/to the storage which must be thread safe + lock (ProcessLocker) + { + var entry = _storage.Get(counterId); + counter = Count(entry, rule); + var expiration = ToTimespan(rule.Period); // default expiration is set for the Period value + if (counter.TotalRequests > rule.Limit) + { + var retryAfter = RetryAfter(counter, rule); // the calculation depends on the counter returned from CountRequests + if (retryAfter > 0) + { + // Rate Limit exceeded, ban period is active + expiration = TimeSpan.FromSeconds(rule.PeriodTimespan); // current state should expire in the storage after ban period + } + else + { + // Ban period elapsed, start counting + _storage.Remove(counterId); // the store can delete the element on its own using an expiration mechanism, but let's force the element to be deleted + counter = new RateLimitCounter(DateTime.UtcNow, null, 1); + } + } + + _storage.Set(counterId, counter, expiration); + } + + return counter; + } + + /// + /// Counts requests based on the current counter state and taking into account the limiting rule. + /// + /// Old counter with starting moment inside. + /// The limiting rule. + /// A value. + public virtual RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule) + { + var now = DateTime.UtcNow; + if (!entry.HasValue) // no entry, start counting + { + return new RateLimitCounter(now, null, 1); // current request is the 1st one + } + + var counter = entry.Value; + var total = counter.TotalRequests + 1; // increment request count + var startedAt = counter.StartedAt; + if (startedAt + ToTimespan(rule.Period) >= now) // counting Period is active + { + var exceededAt = total >= rule.Limit && !counter.ExceededAt.HasValue // current request number equals to the limit + ? now // the exceeding moment is now, the next request will fail but the current one doesn't + : counter.ExceededAt; + return new RateLimitCounter(startedAt, exceededAt, total); // deep copy + } + + var wasExceededAt = counter.ExceededAt; + return wasExceededAt + TimeSpan.FromSeconds(rule.PeriodTimespan) >= now // ban PeriodTimespan is active + ? new RateLimitCounter(startedAt, wasExceededAt, total) // still count + : new RateLimitCounter(now, null, 1); // Ban PeriodTimespan elapsed, start counting NOW! + } + + public virtual RateLimitHeaders GetHeaders(HttpContext context, ClientRequestIdentity identity, RateLimitOptions options) + { + RateLimitHeaders headers; + RateLimitCounter? entry; + lock (ProcessLocker) + { + var counterId = GetStorageKey(identity, options); + entry = _storage.Get(counterId); + } + + var rule = options.RateLimitRule; + if (entry.HasValue) + { + headers = new RateLimitHeaders(context, + limit: rule.Period, + remaining: (rule.Limit - entry.Value.TotalRequests).ToString(), + reset: (entry.Value.StartedAt + ToTimespan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); + } + else + { + headers = new RateLimitHeaders(context, + limit: rule.Period, // TODO Double check + remaining: rule.Limit.ToString(), // TODO Double check + reset: (DateTime.UtcNow + ToTimespan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); + } + + return headers; + } + + public virtual string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options) + { + var key = $"{options.RateLimitCounterPrefix}_{identity.ClientId}_{options.RateLimitRule.Period}_{identity.HttpVerb}_{identity.Path}"; + var idBytes = Encoding.UTF8.GetBytes(key); + + byte[] hashBytes; + using (var algorithm = SHA1.Create()) + { + hashBytes = algorithm.ComputeHash(idBytes); + } + + return BitConverter.ToString(hashBytes).Replace("-", string.Empty); + } + + /// + /// Gets the seconds to wait for the next retry by starting moment and the rule. + /// + /// The method must be called after the one. + /// The counter state. + /// The current rule. + /// An value of seconds. + public virtual double RetryAfter(RateLimitCounter counter, RateLimitRule rule) + { + const double defaultSeconds = 1.0D; // one second + var periodTimespan = rule.PeriodTimespan < defaultSeconds + ? defaultSeconds // allow values which are greater or equal to 1 second + : rule.PeriodTimespan; // good value + var now = DateTime.UtcNow; + if (counter.StartedAt + ToTimespan(rule.Period) >= now) // counting Period is active + { + return counter.TotalRequests < rule.Limit + ? 0.0D // happy path, no need to retry, current request is valid + : counter.ExceededAt.HasValue + ? periodTimespan - (now - counter.ExceededAt.Value).TotalSeconds // minus seconds past + : periodTimespan; // exceeding not yet detected -> let's ban for whole period + } + + if (counter.ExceededAt.HasValue && // limit exceeding was happen + counter.ExceededAt + TimeSpan.FromSeconds(periodTimespan) >= now) // ban PeriodTimespan is active + { + var startedAt = counter.ExceededAt.Value; // ban period was started at + double secondsPast = (now - startedAt).TotalSeconds; + double retryAfter = periodTimespan - secondsPast; + return retryAfter; // it can be negative, which means the wait in PeriodTimespan seconds has ended + } + + return 0.0D; // ban period elapsed, no need to retry, current request is valid + } + + /// + /// Converts to time span from a string, such as "1s", "1m", "1h", "1d". + /// + /// The string value with dimentions: '1s', '1m', '1h', '1d'. + /// A value. + /// By default if the value dimension can't be detected. + public virtual TimeSpan ToTimespan(string timespan) + { + if (string.IsNullOrEmpty(timespan)) + { + return TimeSpan.Zero; + } + + var len = timespan.Length - 1; + var value = timespan.Substring(0, len); + var type = timespan.Substring(len, 1); + + return type switch + { + "d" => TimeSpan.FromDays(double.Parse(value)), + "h" => TimeSpan.FromHours(double.Parse(value)), + "m" => TimeSpan.FromMinutes(double.Parse(value)), + "s" => TimeSpan.FromSeconds(double.Parse(value)), + _ => throw new FormatException($"{timespan} can't be converted to TimeSpan, unknown type {type}"), + }; + } +} diff --git a/src/Ocelot/Request/Mapper/RequestMapper.cs b/src/Ocelot/Request/Mapper/RequestMapper.cs index 883c42350..aa78e0a63 100644 --- a/src/Ocelot/Request/Mapper/RequestMapper.cs +++ b/src/Ocelot/Request/Mapper/RequestMapper.cs @@ -18,10 +18,10 @@ public HttpRequestMessage Map(HttpRequest request, DownstreamRoute downstreamRou Method = MapMethod(request, downstreamRoute), RequestUri = MapUri(request), Version = downstreamRoute.DownstreamHttpVersion, + VersionPolicy = downstreamRoute.DownstreamHttpVersionPolicy, }; MapHeaders(request, requestMessage); - return requestMessage; } @@ -55,7 +55,7 @@ private static void AddContentHeaders(HttpRequest request, HttpContent content) // The performance might be improved by retrieving the matching headers from the request // instead of calling request.Headers.TryGetValue for each used content header - var matchingHeaders = ContentHeaders.Where(header => request.Headers.ContainsKey(header)); + var matchingHeaders = ContentHeaders.Where(request.Headers.ContainsKey); foreach (var key in matchingHeaders) { diff --git a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs index 6440e3d46..f7da43960 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs @@ -35,7 +35,7 @@ public async Task Invoke(HttpContext httpContext) catch (Exception ex) { // TODO Review the error handling, we should throw an exception here and use the global error handler middleware to catch it - httpContext.Items.UpsertErrors([new UnmappableRequestError(ex)]); + httpContext.Items.SetError(new UnmappableRequestError(ex)); return; } diff --git a/src/Ocelot/Requester/MessageInvokerPool.cs b/src/Ocelot/Requester/MessageInvokerPool.cs index 32d9b5235..130b4bccb 100644 --- a/src/Ocelot/Requester/MessageInvokerPool.cs +++ b/src/Ocelot/Requester/MessageInvokerPool.cs @@ -6,11 +6,6 @@ namespace Ocelot.Requester; public class MessageInvokerPool : IMessageInvokerPool { - /// - /// TODO This should be configurable and available as global config parameter in ocelot.json. - /// - public const int DefaultRequestTimeoutSeconds = 90; - private readonly ConcurrentDictionary> _handlersPool; private readonly IDelegatingHandlerHandlerFactory _handlerFactory; private readonly IOcelotLogger _logger; @@ -37,6 +32,18 @@ public HttpMessageInvoker Get(DownstreamRoute downstreamRoute) public void Clear() => _handlersPool.Clear(); + /// + /// TODO This should be configurable and available as global config parameter in ocelot.json. + /// + public const int DefaultRequestTimeoutSeconds = 90; + private int _requestTimeoutSeconds; + + public int RequestTimeoutSeconds + { + get => _requestTimeoutSeconds > 0 ? _requestTimeoutSeconds : DefaultRequestTimeoutSeconds; + set => _requestTimeoutSeconds = value > 0 ? value : DefaultRequestTimeoutSeconds; + } + private HttpMessageInvoker CreateMessageInvoker(DownstreamRoute downstreamRoute) { var baseHandler = CreateHandler(downstreamRoute); @@ -52,7 +59,7 @@ private HttpMessageInvoker CreateMessageInvoker(DownstreamRoute downstreamRoute) // Adding timeout handler to the top of the chain. // It's standard behavior to throw TimeoutException after the defined timeout (90 seconds by default) var timeoutHandler = new TimeoutDelegatingHandler(downstreamRoute.QosOptions.TimeoutValue == 0 - ? TimeSpan.FromSeconds(DefaultRequestTimeoutSeconds) + ? TimeSpan.FromSeconds(RequestTimeoutSeconds) : TimeSpan.FromMilliseconds(downstreamRoute.QosOptions.TimeoutValue)) { InnerHandler = baseHandler, @@ -93,9 +100,14 @@ private HttpMessageHandler CreateHandler(DownstreamRoute downstreamRoute) return handler; } - private readonly struct MessageInvokerCacheKey(DownstreamRoute downstreamRoute) : IEquatable + private readonly struct MessageInvokerCacheKey : IEquatable { - public DownstreamRoute DownstreamRoute { get; } = downstreamRoute; + public MessageInvokerCacheKey(DownstreamRoute downstreamRoute) + { + DownstreamRoute = downstreamRoute; + } + + public DownstreamRoute DownstreamRoute { get; } public override bool Equals(object obj) => obj is MessageInvokerCacheKey key && Equals(key); diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs index c6e500925..e6c49a7d6 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs @@ -1,7 +1,6 @@ using Ocelot.Configuration; using Ocelot.ServiceDiscovery.Providers; -namespace Ocelot.ServiceDiscovery -{ - public delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route); -} +namespace Ocelot.ServiceDiscovery; + +public delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route); diff --git a/src/Ocelot/Values/UpstreamHeaderTemplate.cs b/src/Ocelot/Values/UpstreamHeaderTemplate.cs new file mode 100644 index 000000000..3151fbdf8 --- /dev/null +++ b/src/Ocelot/Values/UpstreamHeaderTemplate.cs @@ -0,0 +1,19 @@ +namespace Ocelot.Values; + +/// +/// Upstream template properties of headers and their regular expression. +/// +/// Ocelot feature: Routing based on request header. +public class UpstreamHeaderTemplate +{ + public string Template { get; } + public string OriginalValue { get; } + public Regex Pattern { get; } + + public UpstreamHeaderTemplate(string template, string originalValue) + { + Template = template; + OriginalValue = originalValue; + Pattern = new Regex(template ?? "$^", RegexOptions.Compiled | RegexOptions.Singleline); + } +} diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index cd90b5f5a..6257e27e6 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -15,6 +15,7 @@ using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Multiplexer; +using System.Text; namespace Ocelot.AcceptanceTests { @@ -42,96 +43,86 @@ public void Should_fix_issue_597() var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key1data/{userid}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port, }, - ], + }, Key = "key1", }, new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key2data/{userid}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port, }, - ], + }, Key = "key2", }, new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key3data/{userid}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port, }, - ], + }, Key = "key3", }, new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key4data/{userid}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port, }, - ], + }, Key = "key4", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { - RouteKeys = - [ - "key1", - "key2", - "key3", - "key4" - ], + RouteKeys = new() { "key1", "key2", "key3", "key4" }, UpstreamPathTemplate = "/EmpDetail/IN/{userid}", }, new FileAggregateRoute { - RouteKeys = - [ - "key1", - "key2" - ], + RouteKeys = new() { "key1", "key2" }, UpstreamPathTemplate = "/EmpDetail/US/{userid}", }, - ], + }, GlobalConfiguration = new FileGlobalConfiguration { RequestIdKey = "CorrelationID", @@ -157,78 +148,73 @@ public void Should_return_response_200_with_advanced_aggregate_configs() var port3 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/Comments", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Comments", }, new FileRoute { DownstreamPathTemplate = "/users/{userId}", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/UserDetails/{userId}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "UserDetails", }, new FileRoute { DownstreamPathTemplate = "/posts/{postId}", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port3, }, - ], + }, UpstreamPathTemplate = "/PostDetails/{postId}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "PostDetails", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Comments", - "UserDetails", - "PostDetails" - ], - RouteKeysConfig = - [ + RouteKeys = new() { "Comments", "UserDetails", "PostDetails" }, + RouteKeysConfig = new() + { new AggregateRouteConfig { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, new AggregateRouteConfig { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, - ], + }, }, - ], + }, }; var userDetailsResponseContent = @"{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""}"; @@ -255,22 +241,22 @@ public void Should_return_response_200_with_simple_url_user_defined_aggregate() var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Laura", }, @@ -278,33 +264,29 @@ public void Should_return_response_200_with_simple_url_user_defined_aggregate() { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Tom", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Laura", - "Tom" - ], + RouteKeys = new() { "Laura", "Tom" }, Aggregator = "FakeDefinedAggregator", }, - ], + }, }; var expected = "Bye from Laura, Bye from Tom"; @@ -347,54 +329,50 @@ public void Should_return_response_200_with_simple_url_one_service_404() var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Tom", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Laura", - "Tom" - ], + RouteKeys = new() { "Laura", "Tom" }, }, - ], + }, }; var expected = "{\"Laura\":,\"Tom\":{Hello from Tom}}"; @@ -417,54 +395,50 @@ public void Should_return_response_200_with_simple_url_both_service_404() var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Tom", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Laura", - "Tom" - ], + RouteKeys = new() { "Laura", "Tom" }, }, - ], + }, }; var expected = "{\"Laura\":,\"Tom\":}"; @@ -487,54 +461,50 @@ public void Should_be_thread_safe() var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Tom", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Laura", - "Tom" - ], + RouteKeys = new() { "Laura", "Tom" }, }, - ], + }, }; this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) @@ -629,114 +599,209 @@ public void Should_return_response_200_with_user_forwarding() } } - private void GivenServiceIsRunning(string baseUrl, int statusCode, string responseBody) + [Fact] + [Trait("Bug", "2039")] + public void Should_return_response_200_with_copied_body_sent_on_multiple_services() { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var route1 = GivenRoute(port1, "/Service1", "Service1", "/Sub1"); + var route2 = GivenRoute(port2, "/Service2", "Service2", "/Sub2"); + var configuration = GivenConfiguration(route1, route2); + var requestBody = @"{""id"":1,""response"":""fromBody-#REPLACESTRING#""}"; + var sub1ResponseContent = @"{""id"":1,""response"":""fromBody-s1""}"; + var sub2ResponseContent = @"{""id"":1,""response"":""fromBody-s2""}"; + var expected = $"{{\"Service1\":{sub1ResponseContent},\"Service2\":{sub2ResponseContent}}}"; + + this.Given(x => x.GivenServiceIsRunning(0, port1, "/Sub1", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s1"))) + .Given(x => x.GivenServiceIsRunning(1, port2, "/Sub2", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s2"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlWithBodyOnTheApiGateway("/", requestBody)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "2039")] + public void Should_return_response_200_with_copied_form_sent_on_multiple_services() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var route1 = GivenRoute(port1, "/Service1", "Service1", "/Sub1"); + var route2 = GivenRoute(port2, "/Service2", "Service2", "/Sub2"); + var configuration = GivenConfiguration(route1, route2); + + var formValues = new[] + { + new KeyValuePair("param1", "value1"), + new KeyValuePair("param2", "from-form-REPLACESTRING"), + }; + + var sub1ResponseContent = "\"[key:param1=value1¶m2=from-form-s1]\""; + var sub2ResponseContent = "\"[key:param1=value1¶m2=from-form-s2]\""; + var expected = $"{{\"Service1\":{sub1ResponseContent},\"Service2\":{sub2ResponseContent}}}"; + + this.Given(x => x.GivenServiceIsRunning(0, port1, "/Sub1", 200, (IFormCollection reqForm) => FormatFormCollection(reqForm).Replace("REPLACESTRING", "s1"))) + .Given(x => x.GivenServiceIsRunning(1, port2, "/Sub2", 200, (IFormCollection reqForm) => FormatFormCollection(reqForm).Replace("REPLACESTRING", "s2"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlWithFormOnTheApiGateway("/", "key", formValues)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + private static string FormatFormCollection(IFormCollection reqForm) + { + var sb = new StringBuilder() + .Append('"'); + + foreach (var kvp in reqForm) + { + sb.Append($"[{kvp.Key}:{kvp.Value}]"); + } + + return sb + .Append('"') + .ToString(); + } + + private void GivenServiceIsRunning(string baseUrl, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); } private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, string responseBody) - { - var baseUrl = $"{Uri.UriSchemeHttp}://localhost:{port}"; - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPaths[index] = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPaths[index] != basePath) + => GivenServiceIsRunning(index, port, basePath, statusCode, + async context => + { + await context.Response.WriteAsync(responseBody); + }); + + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func responseFromBody) + => GivenServiceIsRunning(index, port, basePath, statusCode, + async context => { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didn't match base path"); - } - else + var requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync(); + var responseBody = responseFromBody(requestBody); + await context.Response.WriteAsync(responseBody); + }); + + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func responseFromForm) + => GivenServiceIsRunning(index, port, basePath, statusCode, + async context => { - context.Response.StatusCode = statusCode; + var responseBody = responseFromForm(context.Request.Form); await context.Response.WriteAsync(responseBody); - } - }); + }); + + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Action processContext) + { + var baseUrl = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPaths[index] = !string.IsNullOrEmpty(context.Request.PathBase.Value) + ? context.Request.PathBase.Value + : context.Request.Path.Value; + + if (_downstreamPaths[index] != basePath) + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("downstream path doesn't match base path"); + } + else + { + context.Response.StatusCode = statusCode; + processContext?.Invoke(context); + } + }); } private void GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi() where TAggregator : class, IDefinedAggregator - where TDependency : class - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, true, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(); - s.AddOcelot() - .AddSingletonDefinedAggregator(); - }) - .Configure(a => { a.UseOcelot().Wait(); }); - - _ocelotServer = new TestServer(_webHostBuilder); - _ocelotClient = _ocelotServer.CreateClient(); + where TDependency : class + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(); + s.AddOcelot() + .AddSingletonDefinedAggregator(); + }) + .Configure(a => { a.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); } - private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) - { - _downstreamPaths[0].ShouldBe(expectedDownstreamPathOne); - _downstreamPaths[1].ShouldBe(expectedDownstreamPath); + private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) + { + _downstreamPaths[0].ShouldBe(expectedDownstreamPathOne); + _downstreamPaths[1].ShouldBe(expectedDownstreamPath); } - private static FileRoute GivenRoute(int port, string upstream, string key) => new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = [new FileHostAndPort("localhost", port)], - UpstreamPathTemplate = upstream, - UpstreamHttpMethod = [HttpMethods.Get], - Key = key, + private static FileRoute GivenRoute(int port, string upstream, string key, string downstream = null) => new() + { + DownstreamPathTemplate = downstream ?? "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() { new("localhost", port) }, + UpstreamPathTemplate = upstream, + UpstreamHttpMethod = new() { HttpMethods.Get }, + Key = key, }; - private static new FileConfiguration GivenConfiguration(params FileRoute[] routes) - { - var obj = Steps.GivenConfiguration(routes); - obj.Aggregates.Add( - new() - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - RouteKeys = routes.Select(r => r.Key).ToList(), // [ "Laura", "Tom" ], - } - ); - return obj; - } + private static new FileConfiguration GivenConfiguration(params FileRoute[] routes) + { + var obj = Steps.GivenConfiguration(routes); + obj.Aggregates.Add( + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = routes.Select(r => r.Key).ToList(), // [ "Laura", "Tom" ], + } + ); + return obj; + } } - public class FakeDep - { + public class FakeDep + { } - public class FakeDefinedAggregator : IDefinedAggregator - { - public FakeDefinedAggregator(FakeDep dep) - { - } - - public async Task Aggregate(List responses) - { - var one = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); - var two = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); - - var merge = $"{one}, {two}"; - merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); - var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); - return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); - } + public class FakeDefinedAggregator : IDefinedAggregator + { + public FakeDefinedAggregator(FakeDep dep) + { + } + + public async Task Aggregate(List responses) + { + var one = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); + var two = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); + + var merge = $"{one}, {two}"; + merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); + var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); + return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); + } } -} +} diff --git a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs index 0c2fb65fc..b9e626f00 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs @@ -57,7 +57,7 @@ protected static Client CreateClientWithSecret(string clientId, Secret secret, A protected static Client DefaultClient(AccessTokenType tokenType = AccessTokenType.Jwt, string[] apiScopes = null) { - apiScopes ??= ["api"]; + apiScopes ??= new string[] { "api" }; return new() { ClientId = "client", @@ -65,7 +65,7 @@ protected static Client DefaultClient(AccessTokenType tokenType = AccessTokenTyp ClientSecrets = new List { new("secret".Sha256()) }, AllowedScopes = apiScopes .Union(apiScopes.Select(x => $"{x}.readOnly")) - .Union(["openid", "offline_access"]) + .Union(new string[] { "openid", "offline_access" }) .ToList(), AccessTokenType = tokenType, Enabled = true, @@ -76,8 +76,8 @@ protected static Client DefaultClient(AccessTokenType tokenType = AccessTokenTyp public static IWebHostBuilder CreateIdentityServer(string url, AccessTokenType tokenType, string[] apiScopes, Client[] clients) { - apiScopes ??= ["api"]; - clients ??= [DefaultClient(tokenType, apiScopes)]; + apiScopes ??= new string[] { "api" }; + clients ??= new Client[] { DefaultClient(tokenType, apiScopes) }; var builder = new WebHostBuilder() .UseUrls(url) .UseKestrel() @@ -93,10 +93,10 @@ public static IWebHostBuilder CreateIdentityServer(string url, AccessTokenType t .Select(apiname => new ApiScope(apiname, apiname.ToUpper()))) .AddInMemoryApiResources(apiScopes .Select(x => new { i = Array.IndexOf(apiScopes, x), scope = x }) - .Select(x => CreateApiResource(x.scope, ["openid", "offline_access"]))) + .Select(x => CreateApiResource(x.scope, new string[] { "openid", "offline_access" }))) .AddInMemoryClients(clients) - .AddTestUsers( - [ + .AddTestUsers(new() + { new() { Username = "test", @@ -108,7 +108,7 @@ public static IWebHostBuilder CreateIdentityServer(string url, AccessTokenType t new("LocationId", "321"), }, }, - ]); + }); }) .Configure(app => { @@ -141,16 +141,16 @@ internal Task GivenAuthToken(string url, string apiScope, string cl public static FileRoute GivenDefaultAuthRoute(int port, string upstreamHttpMethod = null, string authProviderKey = null) => new() { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ - new("localhost", port), - ], + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [upstreamHttpMethod ?? HttpMethods.Get], - AuthenticationOptions = new FileAuthenticationOptions + UpstreamHttpMethod = new() { upstreamHttpMethod ?? HttpMethods.Get }, + AuthenticationOptions = new() { - AuthenticationProviderKey = authProviderKey ?? "Test", + AuthenticationProviderKeys = new string[] { authProviderKey ?? "Test" }, }, }; diff --git a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs index ea3e9ee3a..05a60e7e2 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs @@ -112,6 +112,7 @@ public void Should_return_201_using_identity_server_reference_token() .BDDfy(); } + [IgnorePublicMethod] public void GivenThereIsAnIdentityServerOn(string url, AccessTokenType tokenType) { var scopes = new string[] { "api", "api2" }; @@ -127,4 +128,11 @@ public override void Dispose() base.Dispose(); } } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class IgnoreXunitAnalyzersRule1013Attribute : Attribute { } + + [IgnoreXunitAnalyzersRule1013] + [AttributeUsage(AttributeTargets.Method)] + public class IgnorePublicMethodAttribute : Attribute { } } diff --git a/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs b/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs index 204441497..f797431f9 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs @@ -19,9 +19,9 @@ public sealed class MultipleAuthSchemesFeatureTests : AuthenticationSteps, IDisp public MultipleAuthSchemesFeatureTests() : base() { - _identityServers = []; - _identityServerUrls = []; - _tokens = []; + _identityServers = Array.Empty(); + _identityServerUrls = Array.Empty(); + _tokens = Array.Empty(); } public override void Dispose() diff --git a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs index 4eb8a5bf3..c029ad56d 100644 --- a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; +using System.Text; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Ocelot.AcceptanceTests.Caching { @@ -10,6 +12,7 @@ public sealed class CachingTests : IDisposable private const string HelloTomContent = "Hello from Tom"; private const string HelloLauraContent = "Hello from Laura"; + private int _counter = 0; public CachingTests() { @@ -113,6 +116,75 @@ public void Should_not_return_cached_response_as_ttl_expires() .BDDfy(); } + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait("Feat", "2058")] + [Trait("Bug", "2059")] + public void Should_return_different_cached_response_when_request_body_changes_and_EnableContentHashing_is_true(bool asGlobalConfig) + { + var port = PortFinder.GetRandomPort(); + var options = new FileCacheOptions + { + TtlSeconds = 100, + EnableContentHashing = true, + }; + var (testBody1String, testBody2String) = TestBodiesFactory(); + var configuration = GivenFileConfiguration(port, options, asGlobalConfig); + + this.Given(x => x.GivenThereIsAnEchoServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody2String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody2String)) + .And(x => ThenTheCounterValueShouldBe(2)) + .BDDfy(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait("Feat", "2058")] + [Trait("Bug", "2059")] + public void Should_return_same_cached_response_when_request_body_changes_and_EnableContentHashing_is_false(bool asGlobalConfig) + { + var port = PortFinder.GetRandomPort(); + var options = new FileCacheOptions + { + TtlSeconds = 100, + }; + var (testBody1String, testBody2String) = TestBodiesFactory(); + var configuration = GivenFileConfiguration(port, options, asGlobalConfig); + + this.Given(x => x.GivenThereIsAnEchoServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .And(x => ThenTheCounterValueShouldBe(1)) + .BDDfy(); + } + [Fact] [Trait("Issue", "1172")] public void Should_clean_cached_response_by_cache_header_via_new_caching_key() @@ -152,23 +224,25 @@ public void Should_clean_cached_response_by_cache_header_via_new_caching_key() .BDDfy(); } - private static FileConfiguration GivenFileConfiguration(int port, FileCacheOptions cacheOptions) => new() + private static FileConfiguration GivenFileConfiguration(int port, FileCacheOptions cacheOptions, bool asGlobalConfig = false) => new() { - Routes = - [ + Routes = new() + { new FileRoute() { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", port), - ], + }, + DownstreamHttpMethod = "Post", DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod =["Get"], - FileCacheOptions = cacheOptions, + UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post }, + FileCacheOptions = asGlobalConfig ? new FileCacheOptions { TtlSeconds = cacheOptions.TtlSeconds } : cacheOptions, }, - ], + }, + GlobalConfiguration = asGlobalConfig ? new FileGlobalConfiguration { CacheOptions = cacheOptions } : null, }; private static void GivenTheCacheExpires() @@ -196,10 +270,61 @@ private void GivenThereIsAServiceRunningOn(string url, HttpStatusCode statusCode }); } + private void GivenThereIsAnEchoServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + using var streamReader = new StreamReader(context.Request.Body); + var requestBody = await streamReader.ReadToEndAsync(); + + _counter++; + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(requestBody); + }); + } + + private void ThenTheCounterValueShouldBe(int expected) + { + Assert.Equal(expected, _counter); + } + + private (string TestBody1String, string TestBody2String) TestBodiesFactory() + { + var testBody1 = new TestBody + { + Age = 30, + Email = "test.test@email.com", + FirstName = "Jean", + LastName = "Test", + }; + + var testBody1String = JsonSerializer.Serialize(testBody1); + + var testBody2 = new TestBody + { + Age = 31, + Email = "test.test@email.com", + FirstName = "Jean", + LastName = "Test", + }; + + var testBody2String = JsonSerializer.Serialize(testBody2); + + return (testBody1String, testBody2String); + } + public void Dispose() { _serviceHandler?.Dispose(); _steps.Dispose(); } } + + public class TestBody + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + } } diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs deleted file mode 100644 index dad9af3dc..000000000 --- a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs +++ /dev/null @@ -1,217 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; - -namespace Ocelot.AcceptanceTests -{ - public class ClientRateLimitTests : IDisposable - { - private readonly Steps _steps; - private int _counterOne; - private readonly ServiceHandler _serviceHandler; - - public ClientRateLimitTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_call_withratelimiting() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 1000, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - HttpStatusCode = 428, - }, - RequestIdKey = "oceclientrequest", - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .BDDfy(); - } - - [Fact] - public void should_wait_for_period_timespan_to_elapse_before_making_next_request() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 2, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - HttpStatusCode = 428, - }, - RequestIdKey = "oceclientrequest", - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .And(x => _steps.GivenIWait(1000)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .And(x => _steps.GivenIWait(1000)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .BDDfy(); - } - - [Fact] - public void should_call_middleware_withWhitelistClient() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List { "ocelotclient1"}, - Limit = 3, - Period = "1s", - PeriodTimespan = 100, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - }, - RequestIdKey = "oceclientrequest", - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => - { - _counterOne++; - context.Response.StatusCode = 200; - context.Response.WriteAsync(_counterOne.ToString()); - return Task.CompletedTask; - }); - } - - public void Dispose() - { - _steps.Dispose(); - } - } -} diff --git a/test/Ocelot.AcceptanceTests/ContentTests.cs b/test/Ocelot.AcceptanceTests/ContentTests.cs index 5c64da21f..272dc5210 100644 --- a/test/Ocelot.AcceptanceTests/ContentTests.cs +++ b/test/Ocelot.AcceptanceTests/ContentTests.cs @@ -170,20 +170,20 @@ private static string GenerateDummyDatFile(int sizeInMb) private static FileConfiguration GivenConfiguration(int port, string method = null) => new() { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", port), - ], + }, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [method ?? HttpMethods.Get], + UpstreamHttpMethod = new() {method ?? HttpMethods.Get }, }, - ], + }, }; } } diff --git a/test/Ocelot.AcceptanceTests/DefaultVersionPolicyTests.cs b/test/Ocelot.AcceptanceTests/DefaultVersionPolicyTests.cs new file mode 100644 index 000000000..c9a1d1419 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/DefaultVersionPolicyTests.cs @@ -0,0 +1,166 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +namespace Ocelot.AcceptanceTests; + +[Trait("Feat", "1672")] +public sealed class DefaultVersionPolicyTests : Steps +{ + private const string Body = "supercalifragilistic"; + + public DefaultVersionPolicyTests() + { + } + + [Fact] + public void Should_return_bad_gateway_when_request_higher_receive_lower() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionOrHigher); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http1)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .BDDfy(); + } + + [Fact] + public void Should_return_bad_gateway_when_request_lower_receive_higher() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionOrLower); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .BDDfy(); + } + + [Fact] + public void Should_return_bad_gateway_when_request_exact_receive_different() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionExact); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_exact_receive_exact() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionExact); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_lower_receive_lower() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionOrLower); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http1)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_lower_receive_exact() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionOrLower); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_higher_receive_higher() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionOrHigher); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_higher_receive_exact() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionOrHigher); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http1)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private static void GivenThereIsAServiceRunningOn(int port, HttpProtocols protocols) + { + var url = $"{Uri.UriSchemeHttps}://localhost:{port}"; + var builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .ConfigureKestrel(serverOptions => + { + serverOptions.ConfigureEndpointDefaults(listenOptions => { listenOptions.Protocols = protocols; }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .Configure(app => + { + app.Run(async context => + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(Body); + }); + }) + .Build(); + + builder.Start(); + } + + private static FileRoute GivenHttpsRoute(int port, string httpVersion, string versionPolicy) => new() + { + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { HttpMethods.Get }, + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new() { new("localhost", port) }, + DownstreamScheme = Uri.UriSchemeHttps, // !!! + DownstreamHttpVersion = httpVersion, + DownstreamHttpVersionPolicy = versionPolicy, + DangerousAcceptAnyServerCertificateValidator = true, + }; +} diff --git a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs index 83f79e720..330861317 100644 --- a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs @@ -123,7 +123,7 @@ public void should_call_global_di_handlers_multiple_times() this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi()) + .And(x => _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(true)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) diff --git a/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs b/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs new file mode 100644 index 000000000..ea09880df --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs @@ -0,0 +1,611 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Ocelot.Metadata; +using Ocelot.Middleware; +using System.Globalization; + +namespace Ocelot.AcceptanceTests.Metadata; + +[Trait("Feat", "738")] +public class DownstreamMetadataTests : IDisposable +{ + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public enum StringArrayConfig + { + Default = 1, + AlternateSeparators, + AlternateTrimChars, + AlternateStringSplitOptions, + Mix, + } + + public enum NumberConfig + { + Default = 1, + AlternateNumberStyle, + AlternateCulture, + } + + public DownstreamMetadataTests() + { + _steps = new Steps(); + _serviceHandler = new ServiceHandler(); + } + + public void Dispose() + { + _steps?.Dispose(); + _serviceHandler?.Dispose(); + } + + [Theory] + [InlineData(typeof(StringDownStreamMetadataHandler))] + [InlineData(typeof(StringArrayDownStreamMetadataHandler))] + [InlineData(typeof(BoolDownStreamMetadataHandler))] + [InlineData(typeof(DoubleDownStreamMetadataHandler))] + [InlineData(typeof(SuperDataContainerDownStreamMetadataHandler))] + public void ShouldMatchTargetObjects(Type currentType) + { + (Dictionary sourceDictionary, Dictionary _) = + GetSourceAndTargetDictionary(currentType); + + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + Metadata = sourceDictionary, + DelegatingHandlers = new List { currentType.Name, }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(currentType)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + /// + /// Testing the string array type with different configurations. + /// + /// The possible separators. + /// The trimmed characters. + /// If the empty entries should be removed. + /// The current test configuration. + [Theory] + [InlineData(new[] { "," }, new[] { ' ' }, nameof(StringSplitOptions.None), StringArrayConfig.Default)] + [InlineData( + new[] { ";", ".", "," }, + new[] { ' ' }, + nameof(StringSplitOptions.None), + StringArrayConfig.AlternateSeparators)] + [InlineData( + new[] { "," }, + new[] { ' ', ';', ':' }, + nameof(StringSplitOptions.None), + StringArrayConfig.AlternateTrimChars)] + [InlineData( + new[] { "," }, + new[] { ' ' }, + nameof(StringSplitOptions.RemoveEmptyEntries), + StringArrayConfig.AlternateStringSplitOptions)] + [InlineData( + new[] { ";", ".", "," }, + new[] { ' ', '_', ':' }, + nameof(StringSplitOptions.RemoveEmptyEntries), + StringArrayConfig.Mix)] + public void ShouldMatchTargetStringArrayAccordingToConfiguration( + string[] separators, + char[] trimChars, + string stringSplitOption, + StringArrayConfig currentConfig) + { + (Dictionary sourceDictionary, Dictionary _) = + GetSourceAndTargetDictionariesForStringArrayType(currentConfig); + + sourceDictionary.Add(nameof(StringArrayConfig), currentConfig.ToString()); + + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + Metadata = sourceDictionary, + DelegatingHandlers = new List { nameof(StringArrayDownStreamMetadataHandler) }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + MetadataOptions = new FileMetadataOptions + { + Separators = separators, + TrimChars = trimChars, + StringSplitOption = stringSplitOption, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(StringArrayDownStreamMetadataHandler))) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Theory] + [InlineData(NumberStyles.Any, "de-CH", NumberConfig.Default)] + [InlineData(NumberStyles.AllowParentheses | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite | NumberStyles.AllowLeadingSign, "de-CH", NumberConfig.AlternateNumberStyle)] + public void ShouldMatchTargetNumberAccordingToConfiguration( + NumberStyles numberStyles, + string cultureName, + NumberConfig currentConfig) + { + (Dictionary sourceDictionary, Dictionary _) = + GetSourceAndTargetDictionariesForNumberType(); + + sourceDictionary.Add(nameof(NumberConfig), currentConfig.ToString()); + + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + Metadata = sourceDictionary, + DelegatingHandlers = new List { nameof(IntDownStreamMetadataHandler) }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + MetadataOptions = new FileMetadataOptions + { + NumberStyle = numberStyles.ToString(), + CurrentCulture = cultureName, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(IntDownStreamMetadataHandler))) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn( + url, + context => + { + context.Response.StatusCode = 200; + return Task.CompletedTask; + }); + } + + /// + /// Starting ocelot with the delegating handler of type currentType. + /// + /// The current delegating handler type. + /// Throws if delegating handler type doesn't match. + private void GivenOcelotIsRunningWithSpecificHandlerForType(Type currentType) + { + switch (currentType) + { + case { } t when t == typeof(StringDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(StringArrayDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(BoolDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(DoubleDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(SuperDataContainerDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(IntDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + default: + throw new NotImplementedException(); + } + } + + // It would have been better to use a generic method, but it is not possible to use a generic type as a parameter + // for the delegating handler name + private class StringDownStreamMetadataHandler : DownstreamMetadataHandler + { + public StringDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class StringArrayDownStreamMetadataHandler : DownstreamMetadataHandler + { + public StringArrayDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base( + httpContextAccessor) + { + } + } + + private class BoolDownStreamMetadataHandler : DownstreamMetadataHandler + { + public BoolDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class DoubleDownStreamMetadataHandler : DownstreamMetadataHandler + { + public DoubleDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class IntDownStreamMetadataHandler : DownstreamMetadataHandler + { + public IntDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class SuperDataContainerDownStreamMetadataHandler : DownstreamMetadataHandler + { + public SuperDataContainerDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base( + httpContextAccessor) + { + } + } + + /// + /// Simple delegating handler that checks if the metadata is correctly passed to the downstream route + /// and checking if the extension method GetMetadata returns the correct value. + /// + /// The current type. + private class DownstreamMetadataHandler : DelegatingHandler + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public DownstreamMetadataHandler(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var downstreamRoute = _httpContextAccessor.HttpContext?.Items.DownstreamRoute(); + + if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(StringArrayConfig))) + { + var currentConfig = + Enum.Parse(downstreamRoute.MetadataOptions.Metadata[nameof(StringArrayConfig)]); + downstreamRoute.MetadataOptions.Metadata.Remove(nameof(StringArrayConfig)); + + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionariesForStringArrayType(currentConfig); + + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } + } + else if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(NumberConfig))) + { + downstreamRoute.MetadataOptions.Metadata.Remove(nameof(NumberConfig)); + + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionariesForNumberType(); + + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } + } + else + { + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionary(typeof(T)); + + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } + } + + return base.SendAsync(request, cancellationToken); + } + } + + public static (Dictionary SourceDictionary, Dictionary TargetDictionary) + GetSourceAndTargetDictionariesForStringArrayType(StringArrayConfig currentConfig) + { + Dictionary sourceDictionary; + Dictionary targetDictionary; + + if (currentConfig == StringArrayConfig.Default) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1, Value2, Value3" }, + { "Key2", "Value2, Value3, Value4" }, + { "Key3", "Value3, ,Value4, Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.AlternateSeparators) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1; Value2. Value3" }, + { "Key2", "Value2. Value3, Value4" }, + { "Key3", "Value3, ,Value4; Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.AlternateTrimChars) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1; :, Value2 :, Value3 " }, + { "Key2", " Value2, Value3; , Value4" }, + { "Key3", "Value3 , ,Value4, Value5 " }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.AlternateStringSplitOptions) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1, ,Value2, Value3, " }, + { "Key2", "Value2, , ,Value3, Value4, , ," }, + { "Key3", "Value3, ,Value4, , ,Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.Mix) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1; :, Value2. :, Value3 " }, + { "Key2", " Value2_, , , Value3; , Value4" }, + { "Key3", "Value3:; , ,Value4, Value5 " }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + throw new NotImplementedException(); + } + + public static (Dictionary SourceDictionary, Dictionary TargetDictionary) + GetSourceAndTargetDictionariesForNumberType() + { + return ( + new Dictionary + { + { "Key1", "-2" }, { "Key2", " (1000000) " }, { "Key3", "-1000000000 " }, + }, + new Dictionary { { "Key1", -2 }, { "Key2", -1000000 }, { "Key3", -1000000000 } }); + } + + /// + /// Method retrieving the source and target dictionary for the current type. + /// The source value is of type string and the target is of type object. + /// + /// The current type. + /// A source and a target directory to compare the results. + /// Throws if type not found. + public static (Dictionary SourceDictionary, Dictionary TargetDictionary) + GetSourceAndTargetDictionary(Type currentType) + { + Dictionary sourceDictionary; + Dictionary targetDictionary; + if (currentType == typeof(StringDownStreamMetadataHandler) || currentType == typeof(string)) + { + sourceDictionary = new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" }, }; + + targetDictionary = new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" }, }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(StringArrayDownStreamMetadataHandler) || currentType == typeof(string[])) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1, Value2, Value3" }, + { "Key2", "Value2, Value3, Value4" }, + { "Key3", "Value3, ,Value4, Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(BoolDownStreamMetadataHandler) || currentType == typeof(bool?)) + { + sourceDictionary = new Dictionary + { + { "Key1", "true" }, + { "Key2", "false" }, + { "Key3", "null" }, + { "Key4", "disabled" }, + { "Key5", "0" }, + { "Key6", "1" }, + { "Key7", "yes" }, + { "Key8", "enabled" }, + { "Key9", "on" }, + { "Key10", "off" }, + { "Key11", "test" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", true }, + { "Key2", false }, + { "Key3", null }, + { "Key4", false }, + { "Key5", false }, + { "Key6", true }, + { "Key7", true }, + { "Key8", true }, + { "Key9", true }, + { "Key10", false }, + { "Key11", null }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(DoubleDownStreamMetadataHandler) || currentType == typeof(double)) + { + sourceDictionary = new Dictionary { { "Key1", "0.00001" }, { "Key2", "0.00000001" }, }; + + targetDictionary = new Dictionary { { "Key1", 0.00001 }, { "Key2", 0.00000001 }, }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(SuperDataContainerDownStreamMetadataHandler) || + currentType == typeof(SuperDataContainer)) + { + sourceDictionary = new Dictionary + { + { "Key1", "{\"key1\":\"Bonjour\",\"key2\":\"Hello\",\"key3\":0.00001,\"key4\":true}" }, + }; + + targetDictionary = new Dictionary + { + { + "Key1", new SuperDataContainer + { + Key1 = "Bonjour", Key2 = "Hello", Key3 = 0.00001, Key4 = true, + } + }, + }; + + return (sourceDictionary, targetDictionary); + } + + throw new NotImplementedException(); + } + + public class SuperDataContainer + { + public string Key1 { get; set; } + + public string Key2 { get; set; } + + public double Key3 { get; set; } + + public bool? Key4 { get; set; } + + public override bool Equals(object obj) + { + // Check for null and compare run-time types. + if (obj == null || this.GetType() != obj.GetType()) + { + return false; + } + + SuperDataContainer other = (SuperDataContainer)obj; + return Key1 == other.Key1 && Key2 == other.Key2 && Key3.Equals(other.Key3) && Key4 == other.Key4; + } + + // https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = (hash * 23) + (Key1?.GetHashCode() ?? 0); + hash = (hash * 23) + (Key2?.GetHashCode() ?? 0); + hash = (hash * 23) + Key3.GetHashCode(); + hash = (hash * 23) + (Key4?.GetHashCode() ?? 0); + return hash; + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index bb1399943..c997d07c5 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -14,9 +14,9 @@ false false false - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 @@ -30,13 +30,14 @@ - - + + + diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index 9697d2032..5a086019a 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -1,217 +1,259 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.File; +using Ocelot.Requester; +using System.Reflection; -namespace Ocelot.AcceptanceTests +namespace Ocelot.AcceptanceTests; + +public sealed class PollyQoSTests : Steps, IDisposable { - public class PollyQoSTests : IDisposable + private readonly ServiceHandler _serviceHandler; + + public PollyQoSTests() { - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; + _serviceHandler = new ServiceHandler(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); + } - public PollyQoSTests() + private static FileRoute GivenRoute(int port, QoSOptions options, string httpMethod = null, string upstream = null) => new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } + new("localhost", port), + }, + UpstreamPathTemplate = upstream ?? "/", + UpstreamHttpMethod = new() { httpMethod ?? HttpMethods.Get }, + QoSOptions = new(options), + }; - private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) - => new() - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = new() - { - new("localhost", port), - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() {httpMethod}, - QoSOptions = new FileQoSOptions(options), - }, - }, - }; - - [Fact] - public void Should_not_timeout() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(10, 500, 1000, null), HttpMethods.Post); - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void Should_timeout() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 1000, null), HttpMethods.Post); - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 2100)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } - - [Fact] - public void Should_open_circuit_breaker_after_two_exceptions() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(2, 5000, 100000, null)); - - this.Given(x => x.GivenThereIsABrokenServiceRunningOn($"http://localhost:{port}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } - - [Fact] - public void Should_open_circuit_breaker_then_close() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(2, 500, 1000, null)); - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) - .Given(x => _steps.GivenThereIsAConfiguration(configuration)) - .Given(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Open_circuit_should_not_effect_different_route() - { - var port1 = PortFinder.GetRandomPort(); - var port2 = PortFinder.GetRandomPort(); - var qos1 = new QoSOptions(2, 500, 1000, null); - - var configuration = FileConfigurationFactory(port1, qos1); - var route2 = configuration.Routes[0].Clone() as FileRoute; - route2.DownstreamHostAndPorts[0].Port = port2; - route2.UpstreamPathTemplate = "/working"; - route2.QoSOptions = new(); - configuration.Routes.Add(route2); - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}", 200, "Hello from Tom", 0)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - [Trait("Bug", "1833")] - public void Should_timeout_per_default_after_90_seconds() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(new FileQoSOptions()), HttpMethods.Get); - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 95000)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } + [Fact] + public void Should_not_timeout() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(10, 500, 1000, null), HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, string.Empty, 10)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenThePostHasContent("postContent")) + .When(x => WhenIPostUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_timeout() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(0, 0, 1000, null), HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, string.Empty, 2100)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenThePostHasContent("postContent")) + .When(x => WhenIPostUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_after_two_exceptions() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(2, 1000, 100000, null)); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsABrokenServiceRunningOn(port, HttpStatusCode.InternalServerError)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) // opened + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) // Polly status + .BDDfy(); + } + + [Fact] + [Trait("Bug", "2085")] + public void Should_open_circuit_breaker_for_DefaultBreakDuration() + { + int invalidDuration = QoSOptions.LowBreakDuration; // valid value must be >500ms, exact 500ms is invalid + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(2, invalidDuration, 100000, null)); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsABrokenServiceRunningOn(port, HttpStatusCode.InternalServerError)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) // opened + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) // Polly status + .Given(x => GivenIWaitMilliseconds(QoSOptions.DefaultBreakDuration - 500)) // BreakDuration is not elapsed + .When(x => WhenIGetUrlOnTheApiGateway("/")) // still opened + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) // still opened + .Given(x => GivenThereIsABrokenServiceOnline(HttpStatusCode.NotFound)) + .Given(x => GivenIWaitMilliseconds(500)) // BreakDuration should elapse now + .When(x => WhenIGetUrlOnTheApiGateway("/")) // closed, service online + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) // closed, service online + .And(x => ThenTheResponseBodyShouldBe(nameof(HttpStatusCode.NotFound))) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_then_close() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(2, 500, 1000, null)); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn(port, "Hello from Laura")) + .Given(x => GivenThereIsAConfiguration(configuration)) + .Given(x => GivenOcelotIsRunningWithPolly()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => GivenIWaitMilliseconds(3000)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Open_circuit_should_not_effect_different_route() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var qos1 = new QoSOptions(2, 500, 1000, null); + var route = GivenRoute(port1, qos1); + var route2 = GivenRoute(port2, new(new FileQoSOptions()), null, "/working"); + var configuration = GivenConfiguration(route, route2); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn(port1, "Hello from Laura")) + .And(x => x.GivenThereIsAServiceRunningOn(port2, HttpStatusCode.OK, "Hello from Tom", 0)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => WhenIGetUrlOnTheApiGateway("/working")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenIWaitMilliseconds(3000)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "1833")] + public void Should_timeout_per_default_after_90_seconds() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(new FileQoSOptions()), HttpMethods.Get); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, string.Empty, 3500)) // 3.5s > 3s -> ServiceUnavailable + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenIHackDefaultTimeoutValue(3)) // after 3 secs -> Timeout exception aka request cancellation + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } - private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); + private void GivenIHackDefaultTimeoutValue(int defaultTimeoutSeconds) + { + var field = typeof(MessageInvokerPool).GetField("_requestTimeoutSeconds", BindingFlags.NonPublic | BindingFlags.Instance); + var service = _ocelotServer.Services.GetService(typeof(IMessageInvokerPool)); + field.SetValue(service, defaultTimeoutSeconds); // hack the value of default 90 seconds + } + + private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); - private void GivenThereIsABrokenServiceRunningOn(string url) + private HttpStatusCode _brokenServiceStatusCode; + private void GivenThereIsABrokenServiceRunningOn(int port, HttpStatusCode brokenStatusCode) + { + string url = DownstreamUrl(port); + _brokenServiceStatusCode = brokenStatusCode; + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - context.Response.StatusCode = 500; - await context.Response.WriteAsync("this is an exception"); - }); - } + context.Response.StatusCode = (int)_brokenServiceStatusCode; + await context.Response.WriteAsync(_brokenServiceStatusCode.ToString()); + }); + } - private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) - { - var requestCount = 0; - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (requestCount == 2) - { - // in Polly v8 - // MinimumThroughput (ExceptionsAllowedBeforeBreaking) must be 2 or more - // BreakDuration (DurationOfBreak) must be 500 or more - // Timeout (TimeoutValue) must be 1000 or more - // so we wait for 2.1 seconds to make sure the circuit is open - // DurationOfBreak * ExceptionsAllowedBeforeBreaking + Timeout - // 500 * 2 + 1000 = 2000 minimum + 100 milliseconds to exceed the minimum - await Task.Delay(2100); - } - - requestCount++; - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); - }); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) + private void GivenThereIsABrokenServiceOnline(HttpStatusCode onlineStatusCode) + { + _brokenServiceStatusCode = onlineStatusCode; + } + + private void GivenThereIsAPossiblyBrokenServiceRunningOn(int port, string responseBody) + { + var requestCount = 0; + string url = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + if (requestCount == 2) { - Thread.Sleep(timeout); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } + // In Polly v8: + // MinimumThroughput (ExceptionsAllowedBeforeBreaking) must be 2 or more + // BreakDuration (DurationOfBreak) must be 500 or more + // Timeout (TimeoutValue) must be 1000 or more + // So, we wait for 2.1 seconds to make sure the circuit is open + // DurationOfBreak * ExceptionsAllowedBeforeBreaking + Timeout + // 500 * 2 + 1000 = 2000 minimum + 100 milliseconds to exceed the minimum + await Task.Delay(2100); + } - public void Dispose() + requestCount++; + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + }); + } + + private void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, string responseBody, int timeout) + { + string url = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => { - _serviceHandler?.Dispose(); - _steps.Dispose(); - GC.SuppressFinalize(this); - } + Thread.Sleep(timeout); + context.Response.StatusCode = (int)statusCode; + await context.Response.WriteAsync(responseBody); + }); } } diff --git a/test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs b/test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs new file mode 100644 index 000000000..4dd80e7ec --- /dev/null +++ b/test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs @@ -0,0 +1,182 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; + +namespace Ocelot.AcceptanceTests.RateLimiting; + +public sealed class ClientRateLimitingTests : Steps, IDisposable +{ + const int OK = (int)HttpStatusCode.OK; + const int TooManyRequests = (int)HttpStatusCode.TooManyRequests; + + private int _counterOne; + private readonly ServiceHandler _serviceHandler; + + public ClientRateLimitingTests() + { + _serviceHandler = new ServiceHandler(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); + } + + [Fact] + [Trait("Feat", "37")] + public void Should_call_with_rate_limiting() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, null, null, new(), 3, "1s", 1); // periods are equal + var configuration = GivenConfigurationWithRateLimitOptions(route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .BDDfy(); + } + + [Fact] + [Trait("Feat", "37")] + public void Should_wait_for_period_timespan_to_elapse_before_making_next_request() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, "/api/ClientRateLimit?count={count}", "/ClientRateLimit/?{count}", new(), 3, "1s", 2); + var configuration = GivenConfigurationWithRateLimitOptions(route); + _counterOne = 0; + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 2)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .And(x => GivenIWait(1000)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .And(x => GivenIWait(1000)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .And(x => ThenTheResponseBodyShouldBe("4")) // total 4 OK responses + .BDDfy(); + } + + private int _count = 0; + private int Count() => ++_count; + private string Url() => $"/ClientRateLimit/?{Count()}"; + + private void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Func urlDelegate, long times) + { + for (long i = 0; i < times; i++) + { + var url = urlDelegate.Invoke(); + WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(url, 1); + } + } + + [Fact] + [Trait("Feat", "37")] + public void Should_call_middleware_with_white_list_client() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, null, null, whitelist: new() { "ocelotclient1" }, 3, "3s", 2); // main period is greater than ban one + var configuration = GivenConfigurationWithRateLimitOptions(route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "1590")] + public void StatusShouldNotBeEqualTo429_PeriodTimespanValueIsGreaterThanPeriod() + { + _counterOne = 0; + + // Bug scenario + const string period = "1s"; + const double periodTimespan = /*30*/3; // but decrease 30 to 3 secs, "no wasting time" life hack + const long limit = 100L; + + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, "/api/ClientRateLimit?count={count}", "/ClientRateLimit/?{count}", new(), + limit, period, periodTimespan); // bug scenario, adapted + var configuration = GivenConfigurationWithRateLimitOptions(route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + + // main scenario + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, route.RateLimitOptions.Limit)) // 100 times to reach the limit + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .And(x => ThenTheResponseBodyShouldBe(route.RateLimitOptions.Limit.ToString())) // total 100 OK responses + + // extra scenario + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) // 101st request should fail + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .And(x => GivenIWait((int)TimeSpan.FromSeconds(route.RateLimitOptions.PeriodTimespan).TotalMilliseconds)) // in 3 secs PeriodTimespan will elapse + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .And(x => ThenTheResponseBodyShouldBe("101")) // total 101 OK responses + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => + { + _counterOne++; + context.Response.StatusCode = OK; + context.Response.WriteAsync(_counterOne.ToString()); + return Task.CompletedTask; + }); + } + + private FileRoute GivenRoute(int port, string downstream, string upstream, List whitelist, long limit, string period, double periodTimespan) => new() + { + DownstreamPathTemplate = downstream ?? "/api/ClientRateLimit", + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream ?? "/api/ClientRateLimit", + UpstreamHttpMethod = new() { HttpMethods.Get }, + RequestIdKey = RequestIdKey, + RateLimitOptions = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = whitelist ?? new() { "ocelotclient1" }, + Limit = limit, + Period = period ?? "1s", + PeriodTimespan = periodTimespan, + }, + }; + + private static FileConfiguration GivenConfigurationWithRateLimitOptions(params FileRoute[] routes) + { + var config = GivenConfiguration(routes); + config.GlobalConfiguration = new() + { + RateLimitOptions = new() + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = "Exceeding!", + RateLimitCounterPrefix = "ABC", + HttpStatusCode = TooManyRequests, // 429 + }, + RequestIdKey = "OcelotClientRequest", + }; + return config; + } +} diff --git a/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs b/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs index b5c69b006..ea93d6d8c 100644 --- a/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs +++ b/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs @@ -117,12 +117,12 @@ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, Http { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new("localhost", port), - ], + }, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [method ?? HttpMethods.Get], + UpstreamHttpMethod = new() { method ?? HttpMethods.Get }, }; } diff --git a/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs b/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs index 5c20c53e1..1e97a36d1 100644 --- a/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs +++ b/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs @@ -86,22 +86,31 @@ static void options(KestrelServerOptions o) { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new("localhost", port), - ], + }, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [method ?? HttpMethods.Get], + UpstreamHttpMethod = new() { method ?? HttpMethods.Get }, }; } -internal class StreamTestContent(long size, bool sendChunked) : HttpContent +internal class StreamTestContent : HttpContent { - private readonly byte[] _dataBuffer = RandomNumberGenerator.GetBytes(8192); + private readonly long _size; + private readonly bool _sendChunked; + private readonly byte[] _dataBuffer; + + public StreamTestContent(long size, bool sendChunked) + { + _size = size; + _sendChunked = sendChunked; + _dataBuffer = RandomNumberGenerator.GetBytes(8192); + } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { - var remaining = size; + var remaining = _size; while (remaining > 0) { var count = (int)Math.Min(remaining, _dataBuffer.Length); @@ -112,14 +121,14 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon protected override bool TryComputeLength(out long length) { - if (sendChunked) + if (_sendChunked) { length = -1; return false; } else { - length = size; + length = _size; return true; } } diff --git a/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs b/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs index 35c789143..fdb5741ed 100644 --- a/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs +++ b/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs @@ -71,13 +71,13 @@ public void Should_throw_payload_too_large_exception_using_http_sys() private static FileRoute GivenRoute(int port, string method = null) => new() { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new("localhost", port), - ], + }, DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [method ?? HttpMethods.Get], + UpstreamHttpMethod = new() {method ?? HttpMethods.Get }, }; private void GivenThereIsAServiceRunningOn(string baseUrl) diff --git a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs index c2157e24d..a5c1d7c94 100644 --- a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs +++ b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.AcceptanceTests { - public class ReturnsErrorTests : IDisposable + public sealed class ReturnsErrorTests : IDisposable { private readonly Steps _steps; private readonly ServiceHandler _serviceHandler; diff --git a/test/Ocelot.AcceptanceTests/Routing/RoutingBasedOnHeadersTests.cs b/test/Ocelot.AcceptanceTests/Routing/RoutingBasedOnHeadersTests.cs new file mode 100644 index 000000000..eea54640f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Routing/RoutingBasedOnHeadersTests.cs @@ -0,0 +1,464 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; + +namespace Ocelot.AcceptanceTests.Routing; + +[Trait("PR", "1312")] +[Trait("Feat", "360")] +public sealed class RoutingBasedOnHeadersTests : Steps, IDisposable +{ + private string _downstreamPath; + private readonly ServiceHandler _serviceHandler; + + public RoutingBasedOnHeadersTests() + { + _serviceHandler = new(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); + } + + [Fact] + public void Should_match_one_header_value() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, headerValue)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_match_one_header_value_when_more_headers() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader("other", "otherValue")) + .And(x => GivenIAddAHeader(headerName, headerValue)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_match_two_header_values_when_more_headers() + { + var port = PortFinder.GetRandomPort(); + var headerName1 = "country_code"; + var headerValue1 = "PL"; + var headerName2 = "region"; + var headerValue2 = "MAZ"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName1] = headerValue1, + [headerName2] = headerValue2, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName1, headerValue1)) + .And(x => GivenIAddAHeader("other", "otherValue")) + .And(x => GivenIAddAHeader(headerName2, headerValue2)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_not_match_one_header_value() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var anotherHeaderValue = "UK"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, anotherHeaderValue)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_not_match_one_header_value_when_no_headers() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_not_match_two_header_values_when_one_different() + { + var port = PortFinder.GetRandomPort(); + var headerName1 = "country_code"; + var headerValue1 = "PL"; + var headerName2 = "region"; + var headerValue2 = "MAZ"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName1] = headerValue1, + [headerName2] = headerValue2, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName1, headerValue1)) + .And(x => GivenIAddAHeader("other", "otherValue")) + .And(x => GivenIAddAHeader(headerName2, "anothervalue")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_not_match_two_header_values_when_one_not_existing() + { + var port = PortFinder.GetRandomPort(); + var headerName1 = "country_code"; + var headerValue1 = "PL"; + var headerName2 = "region"; + var headerValue2 = "MAZ"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName1] = headerValue1, + [headerName2] = headerValue2, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName1, headerValue1)) + .And(x => GivenIAddAHeader("other", "otherValue")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_not_match_one_header_value_when_header_duplicated() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, headerValue)) + .And(x => GivenIAddAHeader(headerName, "othervalue")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_aggregated_route_match_header_value() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var routeA = GivenRoute(port1, "/a", "Laura"); + var routeB = GivenRoute(port2, "/b", "Tom"); + var route = GivenAggRouteWithUpstreamHeaderTemplates(new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(routeA, routeB); + configuration.Aggregates.Add(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port1, "/a", HttpStatusCode.OK, Hello("Laura"))) + .And(x => GivenThereIsAServiceRunningOn(port2, "/b", HttpStatusCode.OK, Hello("Tom"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, headerValue)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_aggregated_route_not_match_header_value() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var routeA = GivenRoute(port1, "/a", "Laura"); + var routeB = GivenRoute(port2, "/b", "Tom"); + var route = GivenAggRouteWithUpstreamHeaderTemplates(new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(routeA, routeB); + configuration.Aggregates.Add(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port1, "/a", HttpStatusCode.OK, Hello("Laura"))) + .And(x => x.GivenThereIsAServiceRunningOn(port2, "/b", HttpStatusCode.OK, Hello("Tom"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_match_header_placeholder() + { + var port = PortFinder.GetRandomPort(); + var headerName = "Region"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/api.internal-{code}/products", + new() + { + [headerName] = "{header:code}", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api.internal-uk/products", HttpStatusCode.OK, Hello("UK"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "uk")) + .When(x => WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello("UK"))) + .BDDfy(); + } + + [Fact] + public void Should_match_header_placeholder_not_in_downstream_path() + { + var port = PortFinder.GetRandomPort(); + var headerName = "ProductName"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/products-info", + new() + { + [headerName] = "product-{header:everything}", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/products-info", HttpStatusCode.OK, Hello("products"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "product-Camera")) + .When(x => WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello("products"))) + .BDDfy(); + } + + [Fact] + public void Should_distinguish_route_for_different_roles() + { + var port = PortFinder.GetRandomPort(); + var headerName = "Origin"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/products-admin", + new() + { + [headerName] = "admin.xxx.com", + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/products", null); + var configuration = GivenConfiguration(route, route2); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/products-admin", HttpStatusCode.OK, Hello("products admin"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "admin.xxx.com")) + .When(x => WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello("products admin"))) + .BDDfy(); + } + + [Fact] + public void Should_match_header_and_url_placeholders() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/{aa}", "/{country_code}/{version}/{aa}", + new() + { + [headerName] = "start_{header:country_code}_version_{header:version}_end", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/pl/v1/bb", HttpStatusCode.OK, Hello())) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "start_pl_version_v1_end")) + .When(x => WhenIGetUrlOnTheApiGateway("/bb")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_match_header_with_braces() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/", "/aa", + new() + { + [headerName] = "my_{header}", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/aa", HttpStatusCode.OK, Hello())) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "my_{header}")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_match_two_headers_with_the_same_name() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue1 = "PL"; + var headerValue2 = "UK"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, + new() + { + [headerName] = headerValue1 + ";{header:whatever}", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, headerValue1)) + .And(x => GivenIAddAHeader(headerName, headerValue2)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + private static string Hello() => Hello("Jolanta"); + private static string Hello(string who) => $"Hello from {who}"; + + private void GivenThereIsAServiceRunningOn(int port) + => GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, Hello()); + + private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) + { + basePath ??= "/"; + responseBody ??= Hello(); + var baseUrl = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync($"{nameof(_downstreamPath)} is not equal to {nameof(basePath)}"); + } + else + { + context.Response.StatusCode = (int)statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + private void ThenTheDownstreamUrlPathShouldBe(string expected) => _downstreamPath.ShouldBe(expected); + + private static FileRoute GivenRoute(int port, string path = null, string key = null) => new() + { + DownstreamPathTemplate = path ?? "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + UpstreamPathTemplate = path ?? "/", + UpstreamHttpMethod = new() { HttpMethods.Get }, + Key = key, + }; + + private static FileRoute GivenRouteWithUpstreamHeaderTemplates(int port, Dictionary templates) + { + var route = GivenRoute(port); + route.UpstreamHeaderTemplates = templates; + return route; + } + + private static FileRoute GivenRouteWithUpstreamHeaderTemplates(int port, string upstream, string downstream, Dictionary templates) + { + var route = GivenRoute(port); + route.UpstreamHeaderTemplates = templates; + route.UpstreamPathTemplate = upstream ?? "/"; + route.DownstreamPathTemplate = downstream ?? "/"; + return route; + } + + private static FileAggregateRoute GivenAggRouteWithUpstreamHeaderTemplates(Dictionary templates) => new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new() { "Laura", "Tom" }, + UpstreamHeaderTemplates = templates, + }; +} diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/RoutingTests.cs rename to test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs index 6470d365c..388e690d1 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; -namespace Ocelot.AcceptanceTests +namespace Ocelot.AcceptanceTests.Routing { public sealed class RoutingTests : IDisposable { @@ -1210,7 +1210,7 @@ internal void ThenTheDownstreamUrlQueryStringShouldBe(string expectedQueryString new("localhost", port), }, UpstreamPathTemplate = upstream, - UpstreamHttpMethod = [HttpMethods.Get], + UpstreamHttpMethod = new() { HttpMethods.Get }, }, }, }; diff --git a/test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs b/test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs new file mode 100644 index 000000000..ed0f8d424 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs @@ -0,0 +1,322 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; + +namespace Ocelot.AcceptanceTests.Routing; + +public sealed class RoutingWithQueryStringTests : Steps, IDisposable +{ + private readonly ServiceHandler _serviceHandler; + + public RoutingWithQueryStringTests() + { + _serviceHandler = new ServiceHandler(); + } + + public override void Dispose() + { + _serviceHandler?.Dispose(); + base.Dispose(); + } + + [Fact] + public void Should_return_response_200_with_query_string_template() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + "/api/units/{subscriptionId}/{unitId}/updates"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Theory] + [Trait("Bug", "952")] + [InlineData("")] + [InlineData("&x=xxx")] + public void Should_return_200_with_query_string_template_different_keys(string additionalParams) + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/subscriptions/{subscriptionId}/updates?unitId={unit}", + "/api/units/{subscriptionId}/updates?unit={unit}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}{additionalParams}", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names() + { + const string userId = "webley"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/persons?personId={userId}", + "/users?userId={userId}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/persons", $"?personId={userId}", "Hello from @webley")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/users?userId={userId}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from @webley")) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() + { + const string uid = "webley"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/persons?personId={uid}", + "/users?userId={uid}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/persons", $"?personId={uid}&userId={uid}", "Hello from @webley")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/users?userId={uid}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from @webley")) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ_case_sensitive() + { + const string userid = "webley"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/persons?personId={userid}", + "/users?userId={userid}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/persons", $"?personId={userid}&userId={userid}", "Hello from @webley")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/users?userId={userid}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from @webley")) + .BDDfy(); + } + + [Theory] + [Trait("Bug", "1174")] + [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "projectNumber=45&startDate=2019-12-12&endDate=2019-12-12")] + [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00:00:00z%20and%20DateOfSale%20le%202020-03-15T00:00:00z")] + public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expected) + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/contracts?{everythingelse}", + "/contracts?{everythingelse}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/contracts", $"?{expected}", "Hello from @sunilk3")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/contracts?{everythingelse}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from @sunilk3")) + .BDDfy(); + } + + [Fact] + public void Should_return_response_200_with_odata_query_string() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, "/{everything}", "/{everything}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/odata/customers", "?$filter=Name%20eq%20'Sam'", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Should_return_response_200_with_query_string_upstream_template() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/units/{subscriptionId}/{unitId}/updates", + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Should_return_response_404_with_query_string_upstream_template_no_query_string() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/units/{subscriptionId}/{unitId}/updates", + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_return_response_404_with_query_string_upstream_template_different_query_string() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/units/{subscriptionId}/{unitId}/updates", + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_return_response_200_with_query_string_upstream_template_multiple_params() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/units/{subscriptionId}/{unitId}/updates", + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "2002")] + public void Should_map_when_query_parameters_has_same_names_with_placeholder() + { + const string username = "bbenameur"; + const string groupName = "Paris"; + const string roleid = "123456"; + const string everything = "something=9874565"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}", + "/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, + $"/account/{username}/groups/{groupName}/roles", + $"?roleId={roleid}&{everything}", + "Hello from Béchir")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Béchir")) + .BDDfy(); + } + + /// + /// To reproduce 1288: query string should contain the placeholder name and value. + /// + [Fact] + [Trait("Bug", "1288")] + public void Should_copy_query_string_to_downstream_path() + { + var idName = "id"; + var idValue = "3"; + var queryName = idName + "1"; + var queryValue = "2" + idValue + "12"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + $"/cpx/t1/{{{idName}}}", + $"/safe/{{{idName}}}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/safe/{idValue}?{queryName}={queryValue}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private static FileRoute GivenRoute(int port, string downstream, string upstream) => new() + { + DownstreamPathTemplate = downstream, + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + UpstreamPathTemplate = upstream, + UpstreamHttpMethod = new() { HttpMethods.Get }, + }; + + private void GivenThereIsAServiceRunningOn(int port, string basePath, string queryString, string responseBody) + { + var baseUrl = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + if (context.Request.PathBase.Value != basePath || context.Request.QueryString.Value != queryString) + { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = StatusCodes.Status200OK; + await context.Response.WriteAsync(responseBody); + } + }); + } +} diff --git a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs b/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs deleted file mode 100644 index 5c34167ac..000000000 --- a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs +++ /dev/null @@ -1,379 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; - -namespace Ocelot.AcceptanceTests -{ - public class RoutingWithQueryStringTests : IDisposable - { - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - - public RoutingWithQueryStringTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void Should_return_response_200_with_query_string_template() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Theory(DisplayName = "1182: " + nameof(Should_return_200_with_query_string_template_different_keys))] - [InlineData("")] - [InlineData("&x=xxx")] - public void Should_return_200_with_query_string_template_different_keys(string additionalParams) - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unit}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/units/{subscriptionId}/updates?unit={unit}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}{additionalParams}", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Theory(DisplayName = "1174: " + nameof(Should_return_200_and_forward_query_parameters_without_duplicates))] - [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] - [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00:00:00z%20and%20DateOfSale%20le%202020-03-15T00:00:00z")] - public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) - { - var port = PortFinder.GetRandomPort(); - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/contracts?{everythingelse}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() { Host = "localhost", Port = port }, - }, - UpstreamPathTemplate = "/contracts?{everythingelse}", - UpstreamHttpMethod = new() { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/contracts", $"?{expectedOrdered}", "Hello from @sunilk3")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/contracts?{everythingelse}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from @sunilk3")) - .BDDfy(); - } - - [Fact] - public void Should_return_response_200_with_odata_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/odata/customers", "?$filter=Name%20eq%20'Sam'", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Should_return_response_200_with_query_string_upstream_template() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Should_return_response_404_with_query_string_upstream_template_no_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void Should_return_response_404_with_query_string_upstream_template_different_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void Should_return_response_200_with_query_string_upstream_template_multiple_params() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - /// - /// To reproduce 1288: query string should contain the placeholder name and value. - /// - [Fact(DisplayName = "1288: " + nameof(Should_copy_query_string_to_downstream_path))] - public void Should_copy_query_string_to_downstream_path() - { - var idName = "id"; - var idValue = "3"; - var queryName = idName + "1"; - var queryValue = "2" + idValue + "12"; - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new FileRoute - { - DownstreamPathTemplate = $"/cpx/t1/{{{idName}}}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() { Host = "localhost", Port = port }, - }, - UpstreamPathTemplate = $"/safe/{{{idName}}}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/safe/{idValue}?{queryName}={queryValue}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - if ((context.Request.PathBase.Value != basePath) || context.Request.QueryString.Value != queryString) - { - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = StatusCodes.Status200OK; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - GC.SuppressFinalize(this); - } - } -} diff --git a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs index 978626c14..d97d98c09 100644 --- a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs @@ -6,474 +6,474 @@ using Ocelot.Cache; using Ocelot.Configuration.File; using System.Text; - -namespace Ocelot.AcceptanceTests -{ - public class ConsulConfigurationInConsulTests : IDisposable - { - private IWebHost _builder; - private readonly Steps _steps; - private IWebHost _fakeConsulBuilder; - private FileConfiguration _config; - private readonly List _consulServices; - - public ConsulConfigurationInConsulTests() - { - _consulServices = new List(); - _steps = new Steps(); - } - - [Fact] - public void should_return_response_200_with_simple_url() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", string.Empty, 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_load_configuration_out_of_consul() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - var consulConfig = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"}, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_load_configuration_out_of_consul_if_it_is_changed() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - var consulConfig = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"}, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var secondConsulConfig = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/cs/status/awesome", - UpstreamHttpMethod = new List {"Get"}, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) - .Then(x => ThenTheConfigIsUpdatedInOcelot()) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var consulConfig = new FileConfiguration - { - DynamicRoutes = new List - { - new() - { - ServiceName = serviceName, - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 1000, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - HttpStatusCode = 428, - }, - DownstreamScheme = "http", - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .BDDfy(); - } - - private void ThenTheConfigIsUpdatedInOcelot() - { - var result = Wait.WaitFor(20000).Until(() => - { - try - { - _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); - _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK); - _steps.ThenTheResponseBodyShouldBe("Hello from Laura"); - return true; - } - catch (Exception) - { - return false; - } - }); - result.ShouldBeTrue(); - } - - private void GivenTheConsulConfigurationIs(FileConfiguration config) - { - _config = config; - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _consulServices.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _fakeConsulBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - var json = JsonConvert.SerializeObject(_config); - - var bytes = Encoding.UTF8.GetBytes(json); - - var base64 = Convert.ToBase64String(bytes); - - var kvp = new FakeConsulGetResponse(base64); - json = JsonConvert.SerializeObject(new[] { kvp }); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - try - { - var reader = new StreamReader(context.Request.Body); - - // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. - // var json = reader.ReadToEnd(); - var json = await reader.ReadToEndAsync(); - - _config = JsonConvert.DeserializeObject(json); - - var response = JsonConvert.SerializeObject(true); - - await context.Response.WriteAsync(response); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - var json = JsonConvert.SerializeObject(_consulServices); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - }) - .Build(); - - _fakeConsulBuilder.Start(); - } - - public class FakeConsulGetResponse - { - public FakeConsulGetResponse(string value) - { - Value = value; - } - - public int CreateIndex => 100; - public int ModifyIndex => 200; - public int LockIndex => 200; - public string Key => "InternalConfiguration"; - public int Flags => 0; - public string Value { get; } - public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; - } - - private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) - { - _builder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.UsePathBase(basePath); - - app.Run(async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - }) - .Build(); - - _builder.Start(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - - private class FakeCache : IOcelotCache - { - public void Add(string key, FileConfiguration value, TimeSpan ttl, string region) - { - throw new NotImplementedException(); - } - - public FileConfiguration Get(string key, string region) - { - throw new NotImplementedException(); - } - - public void ClearRegion(string region) - { - throw new NotImplementedException(); - } - - public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region) - { - throw new NotImplementedException(); - } - } - } -} + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ConsulConfigurationInConsulTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private IWebHost _fakeConsulBuilder; + private FileConfiguration _config; + private readonly List _consulServices; + + public ConsulConfigurationInConsulTests() + { + _consulServices = new List(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_with_simple_url() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", string.Empty, 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_load_configuration_out_of_consul() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + var consulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_load_configuration_out_of_consul_if_it_is_changed() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + var consulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var secondConsulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status/awesome", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) + .Then(x => ThenTheConfigIsUpdatedInOcelot()) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() + { + var consulPort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var consulConfig = new FileConfiguration + { + DynamicRoutes = new List + { + new() + { + ServiceName = serviceName, + RateLimitRule = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 1000, + }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + RateLimitOptions = new FileRateLimitOptions + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = string.Empty, + RateLimitCounterPrefix = string.Empty, + HttpStatusCode = 428, + }, + DownstreamScheme = "http", + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) + .And(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .BDDfy(); + } + + private void ThenTheConfigIsUpdatedInOcelot() + { + var result = Wait.WaitFor(20000).Until(() => + { + try + { + _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); + _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK); + _steps.ThenTheResponseBodyShouldBe("Hello from Laura"); + return true; + } + catch (Exception) + { + return false; + } + }); + result.ShouldBeTrue(); + } + + private void GivenTheConsulConfigurationIs(FileConfiguration config) + { + _config = config; + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _consulServices.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + var json = JsonConvert.SerializeObject(_config); + + var bytes = Encoding.UTF8.GetBytes(json); + + var base64 = Convert.ToBase64String(bytes); + + var kvp = new FakeConsulGetResponse(base64); + json = JsonConvert.SerializeObject(new[] { kvp }); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + try + { + var reader = new StreamReader(context.Request.Body); + + // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + // var json = reader.ReadToEnd(); + var json = await reader.ReadToEndAsync(); + + _config = JsonConvert.DeserializeObject(json); + + var response = JsonConvert.SerializeObject(true); + + await context.Response.WriteAsync(response); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + var json = JsonConvert.SerializeObject(_consulServices); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + public class FakeConsulGetResponse + { + public FakeConsulGetResponse(string value) + { + Value = value; + } + + public int CreateIndex => 100; + public int ModifyIndex => 200; + public int LockIndex => 200; + public string Key => "InternalConfiguration"; + public int Flags => 0; + public string Value { get; } + public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; + } + + private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.UsePathBase(basePath); + + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + + private class FakeCache : IOcelotCache + { + public void Add(string key, FileConfiguration value, TimeSpan ttl, string region) + { + throw new NotImplementedException(); + } + + public FileConfiguration Get(string key, string region) + { + throw new NotImplementedException(); + } + + public void ClearRegion(string region) + { + throw new NotImplementedException(); + } + + public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs new file mode 100644 index 000000000..b98f93c0d --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -0,0 +1,527 @@ +using Consul; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +namespace Ocelot.AcceptanceTests.ServiceDiscovery; + +public sealed class ConsulServiceDiscoveryTests : Steps, IDisposable +{ + private readonly List _consulServices; + private readonly List _consulNodes; + private int _counterOne; + private int _counterTwo; + private int _counterConsul; + private int _counterNodes; + private static readonly object SyncLock = new(); + private string _downstreamPath; + private string _receivedToken; + private readonly ServiceHandler _serviceHandler; + private readonly ServiceHandler _serviceHandler2; + private readonly ServiceHandler _consulHandler; + + public ConsulServiceDiscoveryTests() + { + _serviceHandler = new ServiceHandler(); + _serviceHandler2 = new ServiceHandler(); + _consulHandler = new ServiceHandler(); + _consulServices = new(); + _consulNodes = new(); + } + + public override void Dispose() + { + _serviceHandler?.Dispose(); + _serviceHandler2?.Dispose(); + _consulHandler?.Dispose(); + } + + [Fact] + public void Should_use_consul_service_discovery_and_load_balance_request() + { + const string serviceName = "product"; + var consulPort = PortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var serviceEntryOne = GivenServiceEntry(port1, serviceName: serviceName); + var serviceEntryTwo = GivenServiceEntry(port2, serviceName: serviceName); + var route = GivenRoute(serviceName: serviceName); + var configuration = GivenServiceDiscovery(consulPort, route); + this.Given(x => x.GivenProductServiceOneIsRunning(DownstreamUrl(port1), 200)) + .And(x => x.GivenProductServiceTwoIsRunning(DownstreamUrl(port2), 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + [Fact] + public void Should_handle_request_to_consul_for_downstream_service_and_make_request() + { + const string serviceName = "web"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); + var configuration = GivenServiceDiscovery(consulPort, route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGateway("/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() + { + const string serviceName = "web"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntry = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + + var configuration = GivenServiceDiscovery(consulPort); + configuration.GlobalConfiguration.DownstreamScheme = "http"; + configuration.GlobalConfiguration.HttpHandlerOptions = new() + { + AllowAutoRedirect = true, + UseCookieContainer = true, + UseTracing = false, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/something", HttpStatusCode.OK, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGateway("/web/something")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Should_use_consul_service_discovery_and_load_balance_request_no_re_routes() + { + const string serviceName = "product"; + var consulPort = PortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var serviceEntry1 = GivenServiceEntry(port1, serviceName: serviceName); + var serviceEntry2 = GivenServiceEntry(port2, serviceName: serviceName); + + var configuration = GivenServiceDiscovery(consulPort); + configuration.GlobalConfiguration.LoadBalancerOptions = new() { Type = nameof(LeastConnection) }; + configuration.GlobalConfiguration.DownstreamScheme = "http"; + + this.Given(x => x.GivenProductServiceOneIsRunning(DownstreamUrl(port1), 200)) + .And(x => x.GivenProductServiceTwoIsRunning(DownstreamUrl(port2), 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry1, serviceEntry2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + [Fact] + public void Should_use_token_to_make_request_to_consul() + { + const string serviceName = "web"; + const string token = "abctoken"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntry = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); + + var configuration = GivenServiceDiscovery(consulPort, route); + configuration.GlobalConfiguration.ServiceDiscoveryProvider.Token = token; + + this.Given(_ => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Laura")) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(_ => GivenThereIsAConfiguration(configuration)) + .And(_ => GivenOcelotIsRunningWithConsul()) + .When(_ => WhenIGetUrlOnTheApiGateway("/home")) + .Then(_ => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(_ => ThenTheTokenIs(token)) + .BDDfy(); + } + + [Fact] + public void Should_send_request_to_service_after_it_becomes_available_in_consul() + { + const string serviceName = "product"; + var consulPort = PortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var serviceEntry1 = GivenServiceEntry(port1, serviceName: serviceName); + var serviceEntry2 = GivenServiceEntry(port2, serviceName: serviceName); + var route = GivenRoute(serviceName: serviceName); + var configuration = GivenServiceDiscovery(consulPort, route); + this.Given(x => x.GivenProductServiceOneIsRunning(DownstreamUrl(port1), 200)) + .And(x => x.GivenProductServiceTwoIsRunning(DownstreamUrl(port2), 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry1, serviceEntry2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .And(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .And(x => WhenIRemoveAService(serviceEntry2)) + .And(x => GivenIResetCounters()) + .And(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => ThenOnlyOneServiceHasBeenCalled()) + .And(x => WhenIAddAServiceBackIn(serviceEntry2)) + .And(x => GivenIResetCounters()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .BDDfy(); + } + + [Fact] + public void Should_handle_request_to_poll_consul_for_downstream_service_and_make_request() + { + const string serviceName = "web"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntry = GivenServiceEntry(servicePort, "localhost", $"web_90_0_2_224_{servicePort}", new[] { "version-v1" }, serviceName); + var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); + var configuration = GivenServiceDiscovery(consulPort, route); + + var sd = configuration.GlobalConfiguration.ServiceDiscoveryProvider; + sd.Type = nameof(PollConsul); + sd.PollingInterval = 0; + sd.Namespace = string.Empty; + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Theory] + [Trait("PR", "1944")] + [Trait("Bugs", "849 1496")] + [InlineData(nameof(LeastConnection))] + [InlineData(nameof(RoundRobin))] + [InlineData(nameof(NoLoadBalancer))] + [InlineData(nameof(CookieStickySessions))] + public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) + { + // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) + // with different ServiceNames (e.g. product-us and product-eu), + // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) + const string serviceNameUS = "product-us"; + const string serviceNameEU = "product-eu"; + var consulPort = PortFinder.GetRandomPort(); + var servicePortUS = PortFinder.GetRandomPort(); + var servicePortEU = PortFinder.GetRandomPort(); + const string upstreamHostUS = "us-shop"; + const string upstreamHostEU = "eu-shop"; + var publicUrlUS = $"http://{upstreamHostUS}"; + var publicUrlEU = $"http://{upstreamHostEU}"; + const string responseBodyUS = "Phone chargers with US plug"; + const string responseBodyEU = "Phone chargers with EU plug"; + var serviceEntryUS = GivenServiceEntry(servicePortUS, serviceName: serviceNameUS, tags: new[] { "US" }); + var serviceEntryEU = GivenServiceEntry(servicePortEU, serviceName: serviceNameEU, tags: new[] { "EU" }); + var routeUS = GivenRoute("/products", "/", serviceNameUS, loadBalancerType, upstreamHostUS); + var routeEU = GivenRoute("/products", "/", serviceNameEU, loadBalancerType, upstreamHostEU); + var configuration = GivenServiceDiscovery(consulPort, routeUS, routeEU); + + // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" + // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" + this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePortUS), "/products", MapGet("/products", responseBodyUS))) + .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePortEU), "/products", MapGet("/products", responseBodyEU))) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyEU)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyEU)) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "954")] + public void Should_return_service_address_by_overridden_service_builder_when_there_is_a_node() + { + const string serviceName = "OpenTestService"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); // 9999 + var serviceEntry = GivenServiceEntry(servicePort, + id: "OPEN_TEST_01", + serviceName: serviceName, + tags: new[] { serviceName }); + var serviceNode = new Node() { Name = "n1" }; // cornerstone of the bug + serviceEntry.Node = serviceNode; + var route = GivenRoute("/api/{url}", "/open/{url}", serviceName, httpMethods: new[] { "POST", "GET" }); + var configuration = GivenServiceDiscovery(consulPort, route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Raman")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(x => x.GivenTheServiceNodesAreRegisteredWithConsul(serviceNode)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) // default services registration results with the bug: "n1" host issue + .When(x => WhenIGetUrlOnTheApiGateway("/open/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .And(x => ThenTheResponseBodyShouldBe("")) + .And(x => ThenConsulShouldHaveBeenCalledTimes(1)) + .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(1)) + + // Override default service builder + .Given(x => GivenOcelotIsRunningWithServices(WithOverriddenConsulServiceBuilder)) + .When(x => WhenIGetUrlOnTheApiGateway("/open/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Raman")) + .And(x => ThenConsulShouldHaveBeenCalledTimes(2)) + .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(2)) + .BDDfy(); + } + + private static void WithOverriddenConsulServiceBuilder(IServiceCollection services) + => services.AddOcelot().AddConsul(); + + public class MyConsulServiceBuilder : DefaultConsulServiceBuilder + { + public MyConsulServiceBuilder(Func configurationFactory, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) + : base(configurationFactory, clientFactory, loggerFactory) { } + + protected override string GetDownstreamHost(ServiceEntry entry, Node node) => entry.Service.Address; + } + + private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() + { + Service = new AgentService + { + Service = serviceName, + Address = address ?? "localhost", + Port = port, + ID = id ?? Guid.NewGuid().ToString(), + Tags = tags ?? Array.Empty(), + }, + }; + + private static FileRoute GivenRoute(string downstream = null, string upstream = null, [CallerMemberName] string serviceName = null, string loadBalancerType = null, string upstreamHost = null, string[] httpMethods = null) => new() + { + DownstreamPathTemplate = downstream ?? "/", + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream ?? "/", + UpstreamHttpMethod = httpMethods != null ? new(httpMethods) : new() { HttpMethods.Get }, + UpstreamHost = upstreamHost, + ServiceName = serviceName, + LoadBalancerOptions = new() { Type = loadBalancerType ?? nameof(LeastConnection) }, + }; + + private static FileConfiguration GivenServiceDiscovery(int consulPort, params FileRoute[] routes) + { + var config = GivenConfiguration(routes); + config.GlobalConfiguration.ServiceDiscoveryProvider = new() + { + Scheme = Uri.UriSchemeHttp, + Host = "localhost", + Port = consulPort, + Type = nameof(Provider.Consul.Consul), + }; + return config; + } + + private void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); + } + + private void WhenIAddAServiceBackIn(ServiceEntry serviceEntry) + { + _consulServices.Add(serviceEntry); + } + + private void ThenOnlyOneServiceHasBeenCalled() + { + _counterOne.ShouldBe(10); + _counterTwo.ShouldBe(0); + } + + private void WhenIRemoveAService(ServiceEntry serviceEntry) + { + _consulServices.Remove(serviceEntry); + } + + private void GivenIResetCounters() + { + _counterOne = 0; + _counterTwo = 0; + _counterConsul = 0; + } + + private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) + { + _counterOne.ShouldBeInRange(bottom, top); + _counterOne.ShouldBeInRange(bottom, top); + } + + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + { + var total = _counterOne + _counterTwo; + total.ShouldBe(expected); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) => _consulServices.AddRange(serviceEntries); + private void GivenTheServiceNodesAreRegisteredWithConsul(params Node[] nodes) => _consulNodes.AddRange(nodes); + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) + { + _consulHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) + { + _receivedToken = values.First(); + } + + // Parse the request path to get the service name + var pathMatch = Regex.Match(context.Request.Path.Value, "/v1/health/service/(?[^/]+)"); + if (pathMatch.Success) + { + _counterConsul++; + + // Use the parsed service name to filter the registered Consul services + var serviceName = pathMatch.Groups["serviceName"].Value; + var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); + var json = JsonConvert.SerializeObject(services); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + return; + } + + if (context.Request.Path.Value == "/v1/catalog/nodes") + { + _counterNodes++; + var json = JsonConvert.SerializeObject(_consulNodes); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private void ThenConsulShouldHaveBeenCalledTimes(int expected) => _counterConsul.ShouldBe(expected); + private void ThenConsulNodesShouldHaveBeenCalledTimes(int expected) => _counterNodes.ShouldBe(expected); + + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterOne++; + response = _counterOne.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterTwo++; + response = _counterTwo.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, HttpStatusCode statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("Downstream path doesn't match base path"); + } + else + { + context.Response.StatusCode = (int)statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + private static RequestDelegate MapGet(string path, string responseBody) => async context => + { + var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + if (downstreamPath == path) + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(responseBody); + } + else + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("Not Found"); + } + }; +} diff --git a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs index dceafc0d4..9afa1b154 100644 --- a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs @@ -5,343 +5,343 @@ using Ocelot.WebSockets; using System.Net.WebSockets; using System.Text; - -namespace Ocelot.AcceptanceTests -{ - public class ConsulWebSocketTests : IDisposable - { - private readonly List _secondRecieved; - private readonly List _firstRecieved; - private readonly List _serviceEntries; - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - - public ConsulWebSocketTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _firstRecieved = new List(); - _secondRecieved = new List(); - _serviceEntries = new List(); - } - - [Fact] - public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() - { - var downstreamPort = PortFinder.GetRandomPort(); - var downstreamHost = "localhost"; - - var secondDownstreamPort = PortFinder.GetRandomPort(); - var secondDownstreamHost = "localhost"; - - var serviceName = "websockets"; - var consulPort = PortFinder.GetRandomPort(); - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = downstreamHost, - Port = downstreamPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = secondDownstreamHost, - Port = secondDownstreamPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var config = new FileConfiguration - { - Routes = new List - { - new() - { - UpstreamPathTemplate = "/", - DownstreamPathTemplate = "/ws", - DownstreamScheme = "ws", - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, - ServiceName = serviceName, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Type = "consul", - }, - }, - }; - - this.Given(_ => _steps.GivenThereIsAConfiguration(config)) - .And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul()) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) - .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) - .When(_ => WhenIStartTheClients()) - .Then(_ => ThenBothDownstreamServicesAreCalled()) - .BDDfy(); - } - - private void ThenBothDownstreamServicesAreCalled() - { - _firstRecieved.Count.ShouldBe(10); - _firstRecieved.ForEach(x => - { - x.ShouldBe("test"); - }); - - _secondRecieved.Count.ShouldBe(10); - _secondRecieved.ForEach(x => - { - x.ShouldBe("chocolate"); - }); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _serviceEntries.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - var json = JsonConvert.SerializeObject(_serviceEntries); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private async Task WhenIStartTheClients() - { - var firstClient = StartClient("ws://localhost:5000/"); - - var secondClient = StartSecondClient("ws://localhost:5000/"); - - await Task.WhenAll(firstClient, secondClient); - } - - private async Task StartClient(string url) - { - IClientWebSocket client = new ClientWebSocketProxy(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - var line = "test"; - for (var i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartSecondClient(string url) - { - await Task.Delay(500); - - IClientWebSocket client = new ClientWebSocketProxy(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - var line = "test"; - for (var i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Echo(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private async Task StartSecondFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Message(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private static async Task Echo(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - private static async Task Message(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var bytes = Encoding.UTF8.GetBytes("chocolate"); - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - public void Dispose() - { - _serviceHandler?.Dispose(); + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ConsulWebSocketTests : IDisposable + { + private readonly List _secondRecieved; + private readonly List _firstRecieved; + private readonly List _serviceEntries; + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public ConsulWebSocketTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _firstRecieved = new List(); + _secondRecieved = new List(); + _serviceEntries = new List(); + } + + [Fact] + public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() + { + var downstreamPort = PortFinder.GetRandomPort(); + var downstreamHost = "localhost"; + + var secondDownstreamPort = PortFinder.GetRandomPort(); + var secondDownstreamHost = "localhost"; + + var serviceName = "websockets"; + var consulPort = PortFinder.GetRandomPort(); + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = downstreamHost, + Port = downstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = secondDownstreamHost, + Port = secondDownstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var config = new FileConfiguration + { + Routes = new List + { + new() + { + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/ws", + DownstreamScheme = "ws", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, + ServiceName = serviceName, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Type = "consul", + }, + }, + }; + + this.Given(_ => _steps.GivenThereIsAConfiguration(config)) + .And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul()) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) + .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) + .When(_ => WhenIStartTheClients()) + .Then(_ => ThenBothDownstreamServicesAreCalled()) + .BDDfy(); + } + + private void ThenBothDownstreamServicesAreCalled() + { + _firstRecieved.Count.ShouldBe(10); + _firstRecieved.ForEach(x => + { + x.ShouldBe("test"); + }); + + _secondRecieved.Count.ShouldBe(10); + _secondRecieved.ForEach(x => + { + x.ShouldBe("chocolate"); + }); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + var json = JsonConvert.SerializeObject(_serviceEntries); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private async Task WhenIStartTheClients() + { + var firstClient = StartClient("ws://localhost:5000/"); + + var secondClient = StartSecondClient("ws://localhost:5000/"); + + await Task.WhenAll(firstClient, secondClient); + } + + private async Task StartClient(string url) + { + IClientWebSocket client = new ClientWebSocketProxy(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartSecondClient(string url) + { + await Task.Delay(500); + + IClientWebSocket client = new ClientWebSocketProxy(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Echo(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private async Task StartSecondFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Message(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private static async Task Echo(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private static async Task Message(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var bytes = Encoding.UTF8.GetBytes("chocolate"); + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + public void Dispose() + { + _serviceHandler?.Dispose(); _steps.Dispose(); - GC.SuppressFinalize(this); - } - } -} + GC.SuppressFinalize(this); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs similarity index 95% rename from test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs index 1b7c1e99f..5ec8f194b 100644 --- a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs @@ -1,281 +1,282 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.LoadBalancers; using Steeltoe.Common.Discovery; - -namespace Ocelot.AcceptanceTests -{ - public class EurekaServiceDiscoveryTests : IDisposable - { - private readonly Steps _steps; - private readonly List _eurekaInstances; - private readonly ServiceHandler _serviceHandler; - - public EurekaServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _eurekaInstances = new List(); - } - + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class EurekaServiceDiscoveryTests : IDisposable + { + private readonly Steps _steps; + private readonly List _eurekaInstances; + private readonly ServiceHandler _serviceHandler; + + public EurekaServiceDiscoveryTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _eurekaInstances = new List(); + } + [Theory] [InlineData(true)] - [InlineData(false)] - public void should_use_eureka_service_discovery_and_make_request(bool dotnetRunningInContainer) - { - Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); - var eurekaPort = 8761; - var serviceName = "product"; + [InlineData(false)] + public void should_use_eureka_service_discovery_and_make_request(bool dotnetRunningInContainer) + { + Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); + var eurekaPort = 8761; + var serviceName = "product"; var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; - - var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, - new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Type = "Eureka", - }, - }, - }; - - this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl)) - .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithEureka()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests))) - .BDDfy(); - } - - private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) - { - foreach (var instance in serviceInstances) - { - _eurekaInstances.Add(instance); - } - } - - private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == "/eureka/apps/") - { - var apps = new List(); - - foreach (var serviceInstance in _eurekaInstances) - { - var a = new Application - { - name = serviceName, - instance = new List - { - new() - { - instanceId = $"{serviceInstance.Host}:{serviceInstance}", - hostName = serviceInstance.Host, - app = serviceName, - ipAddr = "127.0.0.1", - status = "UP", - overriddenstatus = "UNKNOWN", - port = new Port {value = serviceInstance.Port, enabled = "true"}, - securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, - countryId = 1, - dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, - leaseInfo = new LeaseInfo - { - renewalIntervalInSecs = 30, - durationInSecs = 90, - registrationTimestamp = 1457714988223, - lastRenewalTimestamp= 1457716158319, - evictionTimestamp = 0, - serviceUpTimestamp = 1457714988223, - }, - metadata = new Metadata - { - value = "java.util.Collections$EmptyMap", - }, - homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - vipAddress = serviceName, - isCoordinatingDiscoveryServer = "false", - lastUpdatedTimestamp = "1457714988223", - lastDirtyTimestamp = "1457714988172", - actionType = "ADDED", - }, - }, - }; - - apps.Add(a); - } - - var applications = new EurekaApplications - { - applications = new Applications - { - application = apps, - apps__hashcode = "UP_1_", - versions__delta = "1", - }, - }; - - var json = JsonConvert.SerializeObject(applications); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private void GivenEurekaProductServiceOneIsRunning(string url) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests)); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - public void Dispose() + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; + + var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, + new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(LeastConnection) }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Type = "Eureka", + }, + }, + }; + + this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl)) + .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithEureka()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests))) + .BDDfy(); + } + + private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) + { + foreach (var instance in serviceInstances) + { + _eurekaInstances.Add(instance); + } + } + + private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == "/eureka/apps/") + { + var apps = new List(); + + foreach (var serviceInstance in _eurekaInstances) + { + var a = new Application + { + name = serviceName, + instance = new List + { + new() + { + instanceId = $"{serviceInstance.Host}:{serviceInstance}", + hostName = serviceInstance.Host, + app = serviceName, + ipAddr = "127.0.0.1", + status = "UP", + overriddenstatus = "UNKNOWN", + port = new Port {value = serviceInstance.Port, enabled = "true"}, + securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, + countryId = 1, + dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, + leaseInfo = new LeaseInfo + { + renewalIntervalInSecs = 30, + durationInSecs = 90, + registrationTimestamp = 1457714988223, + lastRenewalTimestamp= 1457716158319, + evictionTimestamp = 0, + serviceUpTimestamp = 1457714988223, + }, + metadata = new() + { + value = "java.util.Collections$EmptyMap", + }, + homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + vipAddress = serviceName, + isCoordinatingDiscoveryServer = "false", + lastUpdatedTimestamp = "1457714988223", + lastDirtyTimestamp = "1457714988172", + actionType = "ADDED", + }, + }, + }; + + apps.Add(a); + } + + var applications = new EurekaApplications + { + applications = new Applications + { + application = apps, + apps__hashcode = "UP_1_", + versions__delta = "1", + }, + }; + + var json = JsonConvert.SerializeObject(applications); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private void GivenEurekaProductServiceOneIsRunning(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests)); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } + + public class FakeEurekaService : IServiceInstance + { + public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } - - public class FakeEurekaService : IServiceInstance - { - public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) - { - ServiceId = serviceId; - Host = host; - Port = port; - IsSecure = isSecure; - Uri = uri; - Metadata = metadata; - } - - public string ServiceId { get; } - public string Host { get; } - public int Port { get; } - public bool IsSecure { get; } - public Uri Uri { get; } - public IDictionary Metadata { get; } - } - - public class Port - { - [JsonProperty("$")] - public int value { get; set; } - - [JsonProperty("@enabled")] - public string enabled { get; set; } - } - - public class SecurePort - { - [JsonProperty("$")] - public int value { get; set; } - - [JsonProperty("@enabled")] - public string enabled { get; set; } - } - - public class DataCenterInfo - { - [JsonProperty("@class")] - public string value { get; set; } - - public string name { get; set; } - } - - public class LeaseInfo - { - public int renewalIntervalInSecs { get; set; } - - public int durationInSecs { get; set; } - - public long registrationTimestamp { get; set; } - - public long lastRenewalTimestamp { get; set; } - - public int evictionTimestamp { get; set; } - - public long serviceUpTimestamp { get; set; } - } - - public class Metadata - { - [JsonProperty("@class")] - public string value { get; set; } - } - - public class Instance - { - public string instanceId { get; set; } - public string hostName { get; set; } - public string app { get; set; } - public string ipAddr { get; set; } - public string status { get; set; } - public string overriddenstatus { get; set; } - public Port port { get; set; } - public SecurePort securePort { get; set; } - public int countryId { get; set; } - public DataCenterInfo dataCenterInfo { get; set; } - public LeaseInfo leaseInfo { get; set; } - public Metadata metadata { get; set; } - public string homePageUrl { get; set; } - public string statusPageUrl { get; set; } - public string healthCheckUrl { get; set; } - public string vipAddress { get; set; } - public string isCoordinatingDiscoveryServer { get; set; } - public string lastUpdatedTimestamp { get; set; } - public string lastDirtyTimestamp { get; set; } - public string actionType { get; set; } - } - - public class Application - { - public string name { get; set; } - public List instance { get; set; } - } - - public class Applications - { - public string versions__delta { get; set; } - public string apps__hashcode { get; set; } - public List application { get; set; } - } - - public class EurekaApplications - { - public Applications applications { get; set; } - } -} + ServiceId = serviceId; + Host = host; + Port = port; + IsSecure = isSecure; + Uri = uri; + Metadata = metadata; + } + + public string ServiceId { get; } + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public Uri Uri { get; } + public IDictionary Metadata { get; } + } + + public class Port + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class SecurePort + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class DataCenterInfo + { + [JsonProperty("@class")] + public string value { get; set; } + + public string name { get; set; } + } + + public class LeaseInfo + { + public int renewalIntervalInSecs { get; set; } + + public int durationInSecs { get; set; } + + public long registrationTimestamp { get; set; } + + public long lastRenewalTimestamp { get; set; } + + public int evictionTimestamp { get; set; } + + public long serviceUpTimestamp { get; set; } + } + + public class ValueMetadata + { + [JsonProperty("@class")] + public string value { get; set; } + } + + public class Instance + { + public string instanceId { get; set; } + public string hostName { get; set; } + public string app { get; set; } + public string ipAddr { get; set; } + public string status { get; set; } + public string overriddenstatus { get; set; } + public Port port { get; set; } + public SecurePort securePort { get; set; } + public int countryId { get; set; } + public DataCenterInfo dataCenterInfo { get; set; } + public LeaseInfo leaseInfo { get; set; } + public ValueMetadata metadata { get; set; } + public string homePageUrl { get; set; } + public string statusPageUrl { get; set; } + public string healthCheckUrl { get; set; } + public string vipAddress { get; set; } + public string isCoordinatingDiscoveryServer { get; set; } + public string lastUpdatedTimestamp { get; set; } + public string lastDirtyTimestamp { get; set; } + public string actionType { get; set; } + } + + public class Application + { + public string name { get; set; } + public List instance { get; set; } + } + + public class Applications + { + public string versions__delta { get; set; } + public string apps__hashcode { get; set; } + public List application { get; set; } + } + + public class EurekaApplications + { + public Applications applications { get; set; } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs new file mode 100644 index 000000000..5ca22da6e --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs @@ -0,0 +1,212 @@ +using KubeClient; +using KubeClient.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Provider.Kubernetes; +using System.Runtime.CompilerServices; + +namespace Ocelot.AcceptanceTests.ServiceDiscovery; + +public sealed class KubernetesServiceDiscoveryTests : Steps, IDisposable +{ + private readonly string _kubernetesUrl; + private readonly IKubeApiClient _clientFactory; + private readonly ServiceHandler _serviceHandler; + private readonly ServiceHandler _kubernetesHandler; + private string _receivedToken; + + public KubernetesServiceDiscoveryTests() + { + _kubernetesUrl = DownstreamUrl(PortFinder.GetRandomPort()); //5567 + var option = new KubeClientOptions + { + ApiEndPoint = new Uri(_kubernetesUrl), + AccessToken = "txpc696iUhbVoudg164r93CxDTrKRVWG", + AuthStrategy = KubeAuthStrategy.BearerToken, + AllowInsecure = true, + }; + _clientFactory = KubeApiClient.Create(option); + _serviceHandler = new ServiceHandler(); + _kubernetesHandler = new ServiceHandler(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + _kubernetesHandler.Dispose(); + base.Dispose(); + } + + [Fact] + public void ShouldReturnServicesFromK8s() + { + const string namespaces = nameof(KubernetesServiceDiscoveryTests); + const string serviceName = nameof(ShouldReturnServicesFromK8s); + var servicePort = PortFinder.GetRandomPort(); + var downstreamUrl = DownstreamUrl(servicePort); + var downstream = new Uri(downstreamUrl); + var subsetV1 = new EndpointSubsetV1(); + subsetV1.Addresses.Add(new() + { + Ip = Dns.GetHostAddresses(downstream.Host).Select(x => x.ToString()).First(a => a.Contains('.')), + Hostname = downstream.Host, + }); + subsetV1.Ports.Add(new() + { + Name = downstream.Scheme, + Port = servicePort, + }); + var endpoints = GivenEndpoints(subsetV1); + var route = GivenRouteWithServiceName(namespaces); + var configuration = GivenKubeConfiguration(namespaces, route); + var downstreamResponse = serviceName; + this.Given(x => GivenK8sProductServiceOneIsRunning(downstreamUrl, downstreamResponse)) + .And(x => GivenThereIsAFakeKubernetesProvider(serviceName, namespaces, endpoints)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithKubernetes()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => ThenTheResponseBodyShouldBe(downstreamResponse)) + .And(_ => ThenTheTokenIs("Bearer txpc696iUhbVoudg164r93CxDTrKRVWG")) + .BDDfy(); + } + + [Theory] + [Trait("Feat", "1967")] + [InlineData("", HttpStatusCode.BadGateway)] + [InlineData("http", HttpStatusCode.OK)] + public void ShouldReturnServicesByPortNameAsDownstreamScheme(string downstreamScheme, HttpStatusCode statusCode) + { + const string serviceName = "example-web"; + const string namespaces = "default"; + var servicePort = PortFinder.GetRandomPort(); + var downstreamUrl = DownstreamUrl(servicePort); + var downstream = new Uri(downstreamUrl); + + var subsetV1 = new EndpointSubsetV1(); + subsetV1.Addresses.Add(new() + { + Ip = Dns.GetHostAddresses(downstream.Host).Select(x => x.ToString()).First(a => a.Contains('.')), + Hostname = downstream.Host, + }); + subsetV1.Ports.Add(new() + { + Name = "https", // This service instance is offline -> BadGateway + Port = 443, + }); + subsetV1.Ports.Add(new() + { + Name = downstream.Scheme, // http, should be real scheme + Port = downstream.Port, // not 80, should be real port + }); + var endpoints = GivenEndpoints(subsetV1); + + var route = GivenRouteWithServiceName(namespaces); + 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); + + this.Given(x => GivenK8sProductServiceOneIsRunning(downstreamUrl, nameof(ShouldReturnServicesByPortNameAsDownstreamScheme))) + .And(x => GivenThereIsAFakeKubernetesProvider(serviceName, namespaces, endpoints)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithKubernetes()) + .When(x => WhenIGetUrlOnTheApiGateway("/api/example/1")) + .Then(x => ThenTheStatusCodeShouldBe(statusCode)) + .And(_ => ThenTheResponseBodyShouldBe(downstreamScheme == "http" + ? nameof(ShouldReturnServicesByPortNameAsDownstreamScheme) : string.Empty)) + .And(_ => ThenTheTokenIs("Bearer txpc696iUhbVoudg164r93CxDTrKRVWG")) + .BDDfy(); + } + + private void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); + } + + private EndpointsV1 GivenEndpoints(EndpointSubsetV1 subset, [CallerMemberName] string serviceName = "") + { + var e = new EndpointsV1() + { + Kind = "endpoint", + ApiVersion = "1.0", + Metadata = new() + { + Name = serviceName, + Namespace = nameof(KubernetesServiceDiscoveryTests), + }, + }; + e.Subsets.Add(subset); + return e; + } + + private FileRoute GivenRouteWithServiceName(string serviceNamespace, [CallerMemberName] string serviceName = null) => new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { HttpMethods.Get }, + ServiceName = serviceName, + ServiceNamespace = serviceNamespace, + LoadBalancerOptions = new() { Type = nameof(LeastConnection) }, + }; + + private FileConfiguration GivenKubeConfiguration(string serviceNamespace, params FileRoute[] routes) + { + var u = new Uri(_kubernetesUrl); + var configuration = GivenConfiguration(routes); + configuration.GlobalConfiguration.ServiceDiscoveryProvider = new() + { + Scheme = u.Scheme, + Host = u.Host, + Port = u.Port, + Type = nameof(Kube), + PollingInterval = 0, + Namespace = serviceNamespace, + }; + return configuration; + } + + private void GivenThereIsAFakeKubernetesProvider(string serviceName, string namespaces, EndpointsV1 endpoints) + => _kubernetesHandler.GivenThereIsAServiceRunningOn(_kubernetesUrl, async context => + { + if (context.Request.Path.Value == $"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}") + { + if (context.Request.Headers.TryGetValue("Authorization", out var values)) + { + _receivedToken = values.First(); + } + + var json = JsonConvert.SerializeObject(endpoints); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + + private void GivenOcelotIsRunningWithKubernetes() + => GivenOcelotIsRunningWithServices(s => + { + s.AddOcelot().AddKubernetes(); + s.RemoveAll().AddSingleton(_clientFactory); + }); + + private void GivenK8sProductServiceOneIsRunning(string url, string response) + => _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(response ?? nameof(HttpStatusCode.OK)); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); +} diff --git a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs similarity index 92% rename from test/Ocelot.AcceptanceTests/ServiceFabricTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs index ec952bd42..739b19a7d 100644 --- a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs @@ -1,209 +1,209 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; - -namespace Ocelot.AcceptanceTests -{ - public class ServiceFabricTests : IDisposable - { - private readonly Steps _steps; - private string _downstreamPath; - private readonly ServiceHandler _serviceHandler; - - public ServiceFabricTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ServiceFabricTests : IDisposable + { + private readonly Steps _steps; + private string _downstreamPath; + private readonly ServiceHandler _serviceHandler; + + public ServiceFabricTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + [Fact] - [Trait("PR", "570")] - [Trait("Bug", "555")] - public void should_fix_issue_555() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/a", 200, "Hello from Laura", "b=c")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/a?b=c")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "test=best")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?test=best")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Theory] - [Trait("PR", "722")] + [Trait("PR", "570")] + [Trait("Bug", "555")] + public void should_fix_issue_555() + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{everything}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{everything}", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/a", 200, "Hello from Laura", "b=c")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/a?b=c")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/EquipmentInterfaces", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "test=best")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?test=best")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/EquipmentInterfaces", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Theory] + [Trait("PR", "722")] [Trait("Feat", "721")] - [InlineData("/api/{version}/values", "/values", "Service_{version}/Api", "/Service_1.0/Api/values", "/api/1.0/values?test=best", "test=best")] - [InlineData("/api/{version}/{all}", "/{all}", "Service_{version}/Api", "/Service_1.0/Api/products", "/api/1.0/products?test=the-best-from-Aly", "test=the-best-from-Aly")] - public void should_support_placeholder_in_service_fabric_service_name(string upstream, string downstream, string serviceName, string downstreamUrl, string url, string query) - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = downstream, - DownstreamScheme = "http", - UpstreamPathTemplate = upstream, - UpstreamHttpMethod = ["Get"], - ServiceName = serviceName, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", downstreamUrl, 200, "Hello from Felix Boers", query)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(url)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Felix Boers")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, string expectedQueryString) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = (int)HttpStatusCode.NotFound; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - if (context.Request.QueryString.Value.Contains(expectedQueryString)) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - else - { - context.Response.StatusCode = (int)HttpStatusCode.NotFound; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} + [InlineData("/api/{version}/values", "/values", "Service_{version}/Api", "/Service_1.0/Api/values", "/api/1.0/values?test=best", "test=best")] + [InlineData("/api/{version}/{all}", "/{all}", "Service_{version}/Api", "/Service_1.0/Api/products", "/api/1.0/products?test=the-best-from-Aly", "test=the-best-from-Aly")] + public void should_support_placeholder_in_service_fabric_service_name(string upstream, string downstream, string serviceName, string downstreamUrl, string url, string query) + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new() + { + new() + { + DownstreamPathTemplate = downstream, + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream, + UpstreamHttpMethod = new() { HttpMethods.Get }, + ServiceName = serviceName, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", downstreamUrl, 200, "Hello from Felix Boers", query)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(url)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Felix Boers")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, string expectedQueryString) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + if (context.Request.QueryString.Value.Contains(expectedQueryString)) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + else + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs deleted file mode 100644 index c37333cf2..000000000 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ /dev/null @@ -1,744 +0,0 @@ -using Consul; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using System.Text.RegularExpressions; - -namespace Ocelot.AcceptanceTests -{ - public class ServiceDiscoveryTests : IDisposable - { - private readonly Steps _steps; - private readonly List _consulServices; - private int _counterOne; - private int _counterTwo; - private int _counterConsul; - private static readonly object SyncLock = new(); - private string _downstreamPath; - private string _receivedToken; - private readonly ServiceHandler _serviceHandler; - private readonly ServiceHandler _serviceHandler2; - private readonly ServiceHandler _consulHandler; - - public ServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _serviceHandler2 = new ServiceHandler(); - _consulHandler = new ServiceHandler(); - _steps = new Steps(); - _consulServices = new List(); - } - - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var serviceName = "product"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - DownstreamScheme = "http", - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = true, - UseCookieContainer = true, - UseTracing = false, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() - { - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; - var serviceOnePort = PortFinder.GetRandomPort(); - var serviceTwoPort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; - var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceOnePort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceTwoPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - DownstreamScheme = "http", - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - [Fact] - public void should_use_token_to_make_request_to_consul() - { - var token = "abctoken"; - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "web"; - var servicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Token = token, - }, - }, - }; - - this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(_ => _steps.GivenThereIsAConfiguration(configuration)) - .And(_ => _steps.GivenOcelotIsRunningWithConsul()) - .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); - } - - [Fact] - public void should_send_request_to_service_after_it_becomes_available_in_consul() - { - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .And(x => WhenIRemoveAService(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => ThenOnlyOneServiceHasBeenCalled()) - .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = $"web_90_0_2_224_{downstreamServicePort}", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Type = "PollConsul", - PollingInterval = 0, - Namespace = string.Empty, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Theory] - [Trait("PR", "1944")] - [Trait("Issues", "849 1496")] - [InlineData("LeastConnection")] - [InlineData("RoundRobin")] - [InlineData("NoLoadBalancer")] - [InlineData("CookieStickySessions")] - public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) - { - // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) - // with different ServiceNames (e.g. product-us and product-eu), - // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) - var consulPort = PortFinder.GetRandomPort(); - var servicePortUS = PortFinder.GetRandomPort(); - var servicePortEU = PortFinder.GetRandomPort(); - var serviceNameUS = "product-us"; - var serviceNameEU = "product-eu"; - var downstreamServiceUrlUS = $"http://localhost:{servicePortUS}"; - var downstreamServiceUrlEU = $"http://localhost:{servicePortEU}"; - var upstreamHostUS = "us-shop"; - var upstreamHostEU = "eu-shop"; - var publicUrlUS = $"http://{upstreamHostUS}"; - var publicUrlEU = $"http://{upstreamHostEU}"; - var responseBodyUS = "Phone chargers with US plug"; - var responseBodyEU = "Phone chargers with EU plug"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryUS = new ServiceEntry - { - Service = new AgentService - { - Service = serviceNameUS, - Address = "localhost", - Port = servicePortUS, - ID = Guid.NewGuid().ToString(), - Tags = ["US"], - }, - }; - var serviceEntryEU = new ServiceEntry - { - Service = new AgentService - { - Service = serviceNameEU, - Address = "localhost", - Port = servicePortEU, - ID = Guid.NewGuid().ToString(), - Tags = ["EU"], - }, - }; - - var configuration = new FileConfiguration - { - Routes = - [ - new() - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = ["Get"], - UpstreamHost = upstreamHostUS, - ServiceName = serviceNameUS, - LoadBalancerOptions = new() { Type = loadBalancerType }, - }, - new() - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = ["Get"], - UpstreamHost = upstreamHostEU, - ServiceName = serviceNameEU, - LoadBalancerOptions = new() { Type = loadBalancerType }, - }, - ], - GlobalConfiguration = new() - { - ServiceDiscoveryProvider = new() - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" - // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" - this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(downstreamServiceUrlUS, "/products", MapGet("/products", responseBodyUS))) - .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(downstreamServiceUrlEU, "/products", MapGet("/products", responseBodyEU))) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) - .BDDfy(); - } - - private void ThenTheTokenIs(string token) - { - _receivedToken.ShouldBe(token); - } - - private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) - { - _consulServices.Add(serviceEntryTwo); - } - - private void ThenOnlyOneServiceHasBeenCalled() - { - _counterOne.ShouldBe(10); - _counterTwo.ShouldBe(0); - } - - private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) - { - _consulServices.Remove(serviceEntryTwo); - } - - private void GivenIResetCounters() - { - _counterOne = 0; - _counterTwo = 0; - _counterConsul = 0; - } - - private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) - { - _counterOne.ShouldBeInRange(bottom, top); - _counterOne.ShouldBeInRange(bottom, top); - } - - private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) - { - var total = _counterOne + _counterTwo; - total.ShouldBe(expected); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _consulServices.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) - { - _consulHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) - { - _receivedToken = values.First(); - } - - // Parse the request path to get the service name - var pathMatch = Regex.Match(context.Request.Path.Value, "/v1/health/service/(?[^/]+)"); - if (pathMatch.Success) - { - _counterConsul++; - - // Use the parsed service name to filter the registered Consul services - var serviceName = pathMatch.Groups["serviceName"].Value; - var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); - var json = JsonConvert.SerializeObject(services); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private void ThenConsulShouldHaveBeenCalledTimes(int expected) - { - _counterConsul.ShouldBe(expected); - } - - private void GivenProductServiceOneIsRunning(string url, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - string response; - lock (SyncLock) - { - _counterOne++; - response = _counterOne.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - private void GivenProductServiceTwoIsRunning(string url, int statusCode) - { - _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - string response; - lock (SyncLock) - { - _counterTwo++; - response = _counterTwo.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - private RequestDelegate MapGet(string path, string responseBody) => async context => - { - var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - if (downstreamPath == path) - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); - } - else - { - context.Response.StatusCode = 404; - await context.Response.WriteAsync("Not Found"); - } - }; - - public void Dispose() - { - _serviceHandler?.Dispose(); - _serviceHandler2?.Dispose(); - _consulHandler?.Dispose(); - _steps.Dispose(); - } - } -} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index fe4b2bc7f..9b1e4b927 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -29,6 +29,7 @@ using Serilog.Core; using System.IO.Compression; using System.Net.Http.Headers; +using System.Security.Policy; using System.Text; using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; @@ -190,8 +191,8 @@ protected virtual void DeleteOcelotConfig(params string[] files) { Console.WriteLine(e); } - } - } + } + } public void ThenTheResponseBodyHeaderIs(string key, string value) { @@ -536,7 +537,7 @@ public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() _ocelotClient = _ocelotServer.CreateClient(); } - public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() + public void GivenOcelotIsRunningWithHandlerRegisteredInDi(bool global = false) where TOne : DelegatingHandler { _webHostBuilder = new WebHostBuilder(); @@ -555,7 +556,7 @@ public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() { s.AddSingleton(_webHostBuilder); s.AddOcelot() - .AddDelegatingHandler(true); + .AddDelegatingHandler(global); }) .Configure(a => { a.UseOcelot().Wait(); }); @@ -779,60 +780,10 @@ public void GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appset } public void GivenOcelotIsRunningWithEureka() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddEureka(); - }) - .Configure(app => { app.UseOcelot().Wait(); }); + => GivenOcelotIsRunningWithServices(s => s.AddOcelot().AddEureka()); - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningWithPolly() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddPolly(); - }) - .Configure(app => - { - app.UseOcelot() - .Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } + public void GivenOcelotIsRunningWithPolly() => GivenOcelotIsRunningWithServices(WithPolly); + public static void WithPolly(IServiceCollection services) => services.AddOcelot().AddPolly(); public void WhenIGetUrlOnTheApiGateway(string url) { @@ -844,6 +795,29 @@ public void WhenIGetUrlOnTheApiGatewayAndDontWait(string url) _ocelotClient.GetAsync(url); } + public void WhenIGetUrlWithBodyOnTheApiGateway(string url, string body) + { + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Content = new StringContent(body), + }; + _response = _ocelotClient.SendAsync(request).Result; + } + + public void WhenIGetUrlWithFormOnTheApiGateway(string url, string name, IEnumerable> values) + { + var content = new MultipartFormDataContent(); + var dataContent = new FormUrlEncodedContent(values); + content.Add(dataContent, name); + content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); + + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Content = content, + }; + _response = _ocelotClient.SendAsync(request).Result; + } + public void WhenICancelTheRequest() { _ocelotClient.CancelPendingRequests(); diff --git a/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs b/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs index 4e99d1fe0..95124e6bf 100644 --- a/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs +++ b/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs @@ -52,7 +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)); + httpContext.Items.SetIInternalConfiguration(new InternalConfiguration(new List(), null, null, null, null, null, null, null, null, null)); _httpContext = httpContext; } diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj index bcaf49e12..288d79019 100644 --- a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj @@ -13,9 +13,9 @@ false false false - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/test/Ocelot.Benchmarks/PayloadBenchmarks.cs b/test/Ocelot.Benchmarks/PayloadBenchmarks.cs index 7547ad921..fe776f0ea 100644 --- a/test/Ocelot.Benchmarks/PayloadBenchmarks.cs +++ b/test/Ocelot.Benchmarks/PayloadBenchmarks.cs @@ -38,20 +38,20 @@ public void SetUp() { var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", 51879), - ], + }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod =["Post"], + UpstreamHttpMethod = new() { "Post" }, }, - ], + }, }; GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201); @@ -113,12 +113,12 @@ private static object[] GeneratePayload(int size, string directory, string fileN { var filePath = Path.Combine(directory, fileName); var generateDummy = isJson ? (Func) GenerateDummyJsonFile : GenerateDummyDatFile; - return - [ + return new object[] + { generateDummy(size, filePath), fileName, isJson, - ]; + }; } /// diff --git a/test/Ocelot.Benchmarks/ResponseBenchmarks.cs b/test/Ocelot.Benchmarks/ResponseBenchmarks.cs index f68ecb254..a7c1d85b7 100644 --- a/test/Ocelot.Benchmarks/ResponseBenchmarks.cs +++ b/test/Ocelot.Benchmarks/ResponseBenchmarks.cs @@ -41,20 +41,20 @@ public void SetUp() { var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", 51879), - ], - DownstreamScheme = "http", + }, + DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod =["GET"], + UpstreamHttpMethod = new() { HttpMethods.Get }, }, - ], + }, }; GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201); @@ -115,12 +115,12 @@ private static object[] GeneratePayload(int size, string directory, string fileN { var filePath = Path.Combine(directory, fileName); var generateDummy = isJson ? (Func)GenerateDummyJsonFile : GenerateDummyDatFile; - return - [ + return new object[] + { generateDummy(size, filePath), fileName, isJson, - ]; + }; } /// diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 4fc91e0a8..5ae8d0819 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -118,7 +118,7 @@ public void Should_return_OK_status_and_multiline_indented_json_response_with_js .Then(x => ThenTheResultHaveMultiLineIndentedJson()) .BDDfy(); } - + [Fact] public void Should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() { @@ -878,13 +878,13 @@ private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) { _response.StatusCode.ShouldBe(expectedHttpStatusCode); } - + private void ThenTheResultHaveMultiLineIndentedJson() { const string indent = " "; - const int total = 46, skip = 1; + const int total = 52, skip = 1; var lines = _response.Content.ReadAsStringAsync().Result.Split(Environment.NewLine); - lines.Length.ShouldBe(total); + lines.Length.ShouldBeGreaterThanOrEqualTo(total); lines.First().ShouldNotStartWith(indent); lines.Skip(skip).Take(total - skip - 1).ToList() diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index 996603c55..857f2c0b5 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -14,9 +14,9 @@ false false false - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs b/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs new file mode 100644 index 000000000..f3d251887 --- /dev/null +++ b/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs @@ -0,0 +1,34 @@ +using Ocelot.Logging; +using Ocelot.Middleware; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Ocelot.ManualTest; + +public static class CustomOcelotMiddleware +{ + public static Task Invoke(HttpContext context, Func next) + { + var logger = GetLogger(context); + var downstreamRoute = context.Items.DownstreamRoute(); + + if (downstreamRoute?.MetadataOptions?.Metadata is { } metadata) + { + logger.LogInformation(() => + { + var metadataInJson = JsonSerializer.Serialize(metadata); + var message = $"My custom middleware found some metadata: {metadataInJson}"; + return message; + }); + } + + return next(); + } + + private static IOcelotLogger GetLogger(HttpContext context) + { + var loggerFactory = context.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + return logger; + } +} diff --git a/samples/Docker/Dockerfile b/test/Ocelot.ManualTest/Dockerfile similarity index 98% rename from samples/Docker/Dockerfile rename to test/Ocelot.ManualTest/Dockerfile index dad54b20f..31b4580b3 100644 --- a/samples/Docker/Dockerfile +++ b/test/Ocelot.ManualTest/Dockerfile @@ -1,47 +1,47 @@ -#This is the base image used for any ran images -FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base -WORKDIR /app -EXPOSE 80 - -#This image is used to build the source for the runnable app -#It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples: -#Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx -#Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj -FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder -WORKDIR /build -#First we add only the project files so that we can cache nuget packages with dotnet restore -COPY Ocelot.sln Ocelot.sln -COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj -COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj -COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj -COPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj -COPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj -COPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj -COPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj -COPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj -COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj -COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj -COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj -COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj -COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj - -RUN dotnet restore -#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point -COPY codeanalysis.ruleset codeanalysis.ruleset -COPY src src -COPY test test -ARG build_configuration=Debug -RUN dotnet build --no-restore -c ${build_configuration} -ENTRYPOINT ["dotnet"] - -#This is just for holding the published manual tests... -FROM builder AS manual-test-publish -ARG build_configuration=Debug -RUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest - -#Run manual tests! This is the default run option. -#docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test -FROM base AS manual-test -ENV ASPNETCORE_ENVIRONMENT=Development -COPY --from=manual-test-publish /app . -ENTRYPOINT ["dotnet", "Ocelot.ManualTest.dll"] +#This is the base image used for any ran images +FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base +WORKDIR /app +EXPOSE 80 + +#This image is used to build the source for the runnable app +#It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples: +#Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx +#Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder +WORKDIR /build +#First we add only the project files so that we can cache nuget packages with dotnet restore +COPY Ocelot.sln Ocelot.sln +COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj +COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj +COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj +COPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj +COPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj +COPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj +COPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj +COPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj +COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj + +RUN dotnet restore +#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point +COPY codeanalysis.ruleset codeanalysis.ruleset +COPY src src +COPY test test +ARG build_configuration=Debug +RUN dotnet build --no-restore -c ${build_configuration} +ENTRYPOINT ["dotnet"] + +#This is just for holding the published manual tests... +FROM builder AS manual-test-publish +ARG build_configuration=Debug +RUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest + +#Run manual tests! This is the default run option. +#docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test +FROM base AS manual-test +ENV ASPNETCORE_ENVIRONMENT=Development +COPY --from=manual-test-publish /app . +ENTRYPOINT ["dotnet", "Ocelot.ManualTest.dll"] diff --git a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj index 81aabb2f1..919f6b913 100644 --- a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +++ b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj @@ -9,9 +9,9 @@ Exe Ocelot.ManualTest win-x64;osx-x64 - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 135e7e2c7..eaab89ec9 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -55,7 +55,10 @@ public static void Main(string[] args) .UseIISIntegration() .Configure(app => { - app.UseOcelot().Wait(); + app.UseOcelot(options => + { + options.PreAuthenticationMiddleware = CustomOcelotMiddleware.Invoke; + }).Wait(); }) .Build() .Run(); diff --git a/test/Ocelot.ManualTest/appsettings.json b/test/Ocelot.ManualTest/appsettings.json index e2192fccf..87d3f31b3 100644 --- a/test/Ocelot.ManualTest/appsettings.json +++ b/test/Ocelot.ManualTest/appsettings.json @@ -4,7 +4,8 @@ "LogLevel": { "Default": "Error", "System": "Error", - "Microsoft": "Error" + "Microsoft": "Error", + "Ocelot": "Information" } }, "eureka": { diff --git a/samples/Docker-Compose/docker-compose.yaml b/test/Ocelot.ManualTest/docker-compose.yaml similarity index 95% rename from samples/Docker-Compose/docker-compose.yaml rename to test/Ocelot.ManualTest/docker-compose.yaml index 5236202fd..831368441 100644 --- a/samples/Docker-Compose/docker-compose.yaml +++ b/test/Ocelot.ManualTest/docker-compose.yaml @@ -1,24 +1,24 @@ -version: "3.4" -services: - - tests: - build: - context: . - target: builder - volumes: - - type: bind - source: . - target: /results - command: test --logger:trx -r /results - - benchmarks: - build: - context: . - target: builder - args: - build_configuration: Release - command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4 - - manual-test: - build: . - ports: [ "5000:80" ] +version: "3.4" +services: + + tests: + build: + context: . + target: builder + volumes: + - type: bind + source: . + target: /results + command: test --logger:trx -r /results + + benchmarks: + build: + context: . + target: builder + args: + build_configuration: Release + command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4 + + manual-test: + build: . + ports: [ "5000:80" ] diff --git a/test/Ocelot.ManualTest/ocelot.json b/test/Ocelot.ManualTest/ocelot.json index df8b1f976..8eb947163 100644 --- a/test/Ocelot.ManualTest/ocelot.json +++ b/test/Ocelot.ManualTest/ocelot.json @@ -336,6 +336,21 @@ ], "UpstreamPathTemplate": "/bbc/", "UpstreamHttpMethod": [ "Get" ] + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/list-post", + "UpstreamHttpMethod": [ "GET" ], + "Metadata": { + "api_id": "e99d7ce0-d918-443e-b243-1960a8212b5d" + } } ], diff --git a/test/Ocelot.Testing/PortFinder.cs b/test/Ocelot.Testing/PortFinder.cs index 4b661ada7..6eb6b64d4 100644 --- a/test/Ocelot.Testing/PortFinder.cs +++ b/test/Ocelot.Testing/PortFinder.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; diff --git a/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs index 838c6f239..15c827662 100644 --- a/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs +++ b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Administration { - public class OcelotAdministrationBuilderTests + public class OcelotAdministrationBuilderTests : UnitTest { private readonly IServiceCollection _services; private IServiceProvider _serviceProvider; @@ -22,7 +22,7 @@ public OcelotAdministrationBuilderTests() _services = new ServiceCollection(); _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); - } + } private static IWebHostEnvironment GetHostingEnvironment() { diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs index 2201722eb..0bd9eb603 100644 --- a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs @@ -12,7 +12,7 @@ namespace Ocelot.UnitTests.Authentication { - public class AuthenticationMiddlewareTests + public class AuthenticationMiddlewareTests : UnitTest { private readonly Mock _authentication; private readonly Mock _factory; @@ -121,7 +121,7 @@ public void Should_provide_backward_compatibility_if_route_has_several_options_a { var options = new AuthenticationOptions(null, "Test", - [string.Empty, "Fail", "Test"] + new string[] { string.Empty, "Fail", "Test" } ); var methods = new List { "Get" }; this.Given(x => GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() @@ -143,7 +143,7 @@ public void Should_provide_backward_compatibility_if_route_has_several_options_a public void Should_not_call_next_middleware_and_return_no_result_if_all_multiple_keys_were_failed() { var options = new AuthenticationOptions(null, null, - [string.Empty, "Fail", "Fail", "UnknownScheme"] + new string[] { string.Empty, "Fail", "Fail", "UnknownScheme" } ); var methods = new List { "Get" }; diff --git a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs index 045257e32..717de3daf 100644 --- a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Authorization { - public class AuthorizationMiddlewareTests + public class AuthorizationMiddlewareTests : UnitTest { private readonly Mock _authService; private readonly Mock _authScopesService; diff --git a/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs b/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs index 45865a456..6033d0a7a 100644 --- a/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs +++ b/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Authorization { - public class ClaimsAuthorizerTests + public class ClaimsAuthorizerTests : UnitTest { private readonly ClaimsAuthorizer _claimsAuthorizer; private ClaimsPrincipal _claimsPrincipal; diff --git a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs b/test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs similarity index 73% rename from test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs rename to test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs index 841ff5166..f3584e714 100644 --- a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs +++ b/test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs @@ -1,59 +1,60 @@ -using Ocelot.Cache; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; - -namespace Ocelot.UnitTests.Cache -{ - public class RegionCreatorTests - { - private string _result; - private FileRoute _route; - - [Fact] - public void should_create_region() - { - var route = new FileRoute - { - UpstreamHttpMethod = new List { "Get" }, - UpstreamPathTemplate = "/testdummy", - }; - - this.Given(_ => GivenTheRoute(route)) - .When(_ => WhenICreateTheRegion()) - .Then(_ => ThenTheRegionIs("Gettestdummy")) - .BDDfy(); - } - - [Fact] - public void should_use_region() - { - var route = new FileRoute - { - FileCacheOptions = new FileCacheOptions - { - Region = "region", - }, - }; - - this.Given(_ => GivenTheRoute(route)) - .When(_ => WhenICreateTheRegion()) - .Then(_ => ThenTheRegionIs("region")) - .BDDfy(); - } - - private void GivenTheRoute(FileRoute route) - { - _route = route; - } - - private void WhenICreateTheRegion() - { - var regionCreator = new RegionCreator(); - _result = regionCreator.Create(_route); - } - - private void ThenTheRegionIs(string expected) - { - _result.ShouldBe(expected); - } - } -} + +namespace Ocelot.UnitTests.Cache +{ + public class CacheOptionsCreatorTests : UnitTest + { + private CacheOptions _cacheOptions; + private FileRoute _route; + + [Fact] + public void should_create_region() + { + var route = new FileRoute + { + UpstreamHttpMethod = new List { "Get" }, + UpstreamPathTemplate = "/testdummy", + }; + + this.Given(_ => GivenTheRoute(route)) + .When(_ => WhenICreateTheRegion()) + .Then(_ => ThenTheRegionIs("Gettestdummy")) + .BDDfy(); + } + + [Fact] + public void should_use_region() + { + var route = new FileRoute + { + FileCacheOptions = new FileCacheOptions + { + Region = "region", + }, + }; + + this.Given(_ => GivenTheRoute(route)) + .When(_ => WhenICreateTheRegion()) + .Then(_ => ThenTheRegionIs("region")) + .BDDfy(); + } + + private void GivenTheRoute(FileRoute route) + { + _route = route; + } + + private void WhenICreateTheRegion() + { + var cacheOptionsCreator = new CacheOptionsCreator(); + _cacheOptions = cacheOptionsCreator.Create(_route.FileCacheOptions, new FileGlobalConfiguration(), _route.UpstreamPathTemplate, _route.UpstreamHttpMethod); + } + + private void ThenTheRegionIs(string expected) + { + _cacheOptions.Region.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs b/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs index a81ec7d07..50c7de6c9 100644 --- a/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs +++ b/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Cache; -public sealed class DefaultCacheKeyGeneratorTests : IDisposable +public sealed class DefaultCacheKeyGeneratorTests : UnitTest, IDisposable { private readonly ICacheKeyGenerator _cacheKeyGenerator; private readonly HttpRequestMessage _request; @@ -59,7 +59,7 @@ public void should_generate_cache_key_without_request_content() [Fact] public void should_generate_cache_key_with_cache_options_header() { - CacheOptions options = new CacheOptions(100, "region", headerName); + CacheOptions options = new CacheOptions(100, "region", headerName, false); var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{header}"); this.Given(x => x.GivenDownstreamRoute(options)) diff --git a/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs b/test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs similarity index 90% rename from test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs rename to test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs index 573c076ce..e5d14dbe4 100644 --- a/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs +++ b/test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs @@ -3,13 +3,13 @@ namespace Ocelot.UnitTests.Cache { - public class AspMemoryCacheTests + public class DefaultMemoryCacheTests : UnitTest { - private readonly AspMemoryCache _cache; + private readonly DefaultMemoryCache _cache; - public AspMemoryCacheTests() + public DefaultMemoryCacheTests() { - _cache = new AspMemoryCache(new MemoryCache(new MemoryCacheOptions())); + _cache = new DefaultMemoryCache(new MemoryCache(new MemoryCacheOptions())); } [Fact] diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index bb7e829a9..ab00e7675 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Cache { - public class OutputCacheMiddlewareTests + public class OutputCacheMiddlewareTests : UnitTest { private readonly Mock> _cache; private readonly Mock _loggerFactory; @@ -106,7 +106,7 @@ private void GivenTheDownstreamRouteIs() var route = new RouteBuilder() .WithDownstreamRoute(new DownstreamRouteBuilder() .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken", null)) + .WithCacheOptions(new CacheOptions(100, "kanken", null, false)) .WithUpstreamHttpMethod(new List { "Get" }) .Build()) .WithUpstreamHttpMethod(new List { "Get" }) diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs index 9d980aa23..410c73b64 100644 --- a/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.CacheManager { - public class OcelotBuilderExtensionsTests + public class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs index 82ffc5bf9..a2a662029 100644 --- a/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs +++ b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.CacheManager { - public class OcelotCacheManagerCache + public class OcelotCacheManagerCache : UnitTest { private readonly OcelotCacheManagerCache _ocelotOcelotCacheManager; private readonly Mock> _mockCacheManager; diff --git a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs index ee52d3785..76fe0977d 100644 --- a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.CacheManager { - public class OutputCacheMiddlewareRealCacheTests + public class OutputCacheMiddlewareRealCacheTests : UnitTest { private readonly IOcelotCache _cacheManager; private readonly ICacheKeyGenerator _cacheKeyGenerator; @@ -77,7 +77,7 @@ private void GivenTheDownstreamRouteIs() { var route = new DownstreamRouteBuilder() .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken", null)) + .WithCacheOptions(new CacheOptions(100, "kanken", null, false)) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); diff --git a/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs index c3d2c212e..362df7f4a 100644 --- a/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs +++ b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Claims { - public class AddClaimsToRequestTests + public class AddClaimsToRequestTests : UnitTest { private readonly AddClaimsToRequest _addClaimsToRequest; private readonly Mock _parser; diff --git a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs index e996968af..2fcface83 100644 --- a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Claims { - public class ClaimsToClaimsMiddlewareTests + public class ClaimsToClaimsMiddlewareTests : UnitTest { private readonly Mock _addHeaders; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs index d4605f038..3d4c3ff63 100644 --- a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs @@ -6,20 +6,24 @@ namespace Ocelot.UnitTests.Configuration { - public class AggregatesCreatorTests + public class AggregatesCreatorTests : UnitTest { private readonly AggregatesCreator _creator; - private readonly Mock _utpCreator; + private readonly Mock _utpCreator; + private readonly Mock _uhtpCreator; private FileConfiguration _fileConfiguration; private List _routes; private List _result; private UpstreamPathTemplate _aggregate1Utp; - private UpstreamPathTemplate _aggregate2Utp; + private UpstreamPathTemplate _aggregate2Utp; + private Dictionary _headerTemplates1; + private Dictionary _headerTemplates2; public AggregatesCreatorTests() { - _utpCreator = new Mock(); - _creator = new AggregatesCreator(_utpCreator.Object); + _utpCreator = new Mock(); + _uhtpCreator = new Mock(); + _creator = new AggregatesCreator(_utpCreator.Object, _uhtpCreator.Object); } [Fact] @@ -82,7 +86,8 @@ public void should_create_aggregates() this.Given(_ => GivenThe(fileConfig)) .And(_ => GivenThe(routes)) - .And(_ => GivenTheUtpCreatorReturns()) + .And(_ => GivenTheUtpCreatorReturns()) + .And(_ => GivenTheUhtpCreatorReturns()) .When(_ => WhenICreate()) .Then(_ => ThenTheUtpCreatorIsCalledCorrectly()) .And(_ => ThenTheAggregatesAreCreated()) @@ -96,14 +101,16 @@ private void ThenTheAggregatesAreCreated() _result[0].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); _result[0].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[0].UpstreamHost); - _result[0].UpstreamTemplatePattern.ShouldBe(_aggregate1Utp); + _result[0].UpstreamTemplatePattern.ShouldBe(_aggregate1Utp); + _result[0].UpstreamHeaderTemplates.ShouldBe(_headerTemplates1); _result[0].Aggregator.ShouldBe(_fileConfiguration.Aggregates[0].Aggregator); _result[0].DownstreamRoute.ShouldContain(x => x == _routes[0].DownstreamRoute[0]); _result[0].DownstreamRoute.ShouldContain(x => x == _routes[1].DownstreamRoute[0]); _result[1].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); _result[1].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[1].UpstreamHost); - _result[1].UpstreamTemplatePattern.ShouldBe(_aggregate2Utp); + _result[1].UpstreamTemplatePattern.ShouldBe(_aggregate2Utp); + _result[1].UpstreamHeaderTemplates.ShouldBe(_headerTemplates2); _result[1].Aggregator.ShouldBe(_fileConfiguration.Aggregates[1].Aggregator); _result[1].DownstreamRoute.ShouldContain(x => x == _routes[2].DownstreamRoute[0]); _result[1].DownstreamRoute.ShouldContain(x => x == _routes[3].DownstreamRoute[0]); @@ -123,6 +130,16 @@ private void GivenTheUtpCreatorReturns() _utpCreator.SetupSequence(x => x.Create(It.IsAny())) .Returns(_aggregate1Utp) .Returns(_aggregate2Utp); + } + + private void GivenTheUhtpCreatorReturns() + { + _headerTemplates1 = new Dictionary(); + _headerTemplates2 = new Dictionary(); + + _uhtpCreator.SetupSequence(x => x.Create(It.IsAny())) + .Returns(_headerTemplates1) + .Returns(_headerTemplates2); } private void ThenTheResultIsEmpty() diff --git a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs index b1830ba62..d0e8d599e 100644 --- a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs @@ -46,7 +46,7 @@ public void Create_OptionsObjIsNotNull_CreatedSuccessfully(bool isAuthentication // Arrange string authenticationProviderKey = !isAuthenticationProviderKeys ? "Test" : null; string[] authenticationProviderKeys = isAuthenticationProviderKeys ? - ["Test #1", "Test #2"] : null; + new string[] { "Test #1", "Test #2" } : null; var fileRoute = new FileRoute() { AuthenticationOptions = new FileAuthenticationOptions diff --git a/test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs new file mode 100644 index 000000000..12ed54cdf --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs @@ -0,0 +1,100 @@ +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +namespace Ocelot.UnitTests.Configuration; + +[Trait("Feat", "2058")] +[Trait("Bug", "2059")] +public class CacheOptionsCreatorTests +{ + [Fact] + public void ShouldCreateCacheOptions() + { + var options = FileCacheOptionsFactory(); + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(options, null, null, null); + + result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); + result.Region.ShouldBe(options.Region); + result.Header.ShouldBe(options.Header); + result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); + } + + [Fact] + public void ShouldCreateCacheOptionsUsingGlobalConfiguration() + { + var global = GlobalConfigurationFactory(); + + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(new FileCacheOptions(), global, null, null); + + result.TtlSeconds.ShouldBe(global.CacheOptions.TtlSeconds.Value); + result.Region.ShouldBe(global.CacheOptions.Region); + result.Header.ShouldBe(global.CacheOptions.Header); + result.EnableContentHashing.ShouldBe(global.CacheOptions.EnableContentHashing.Value); + } + + [Fact] + public void RouteCacheOptionsShouldOverrideGlobalConfiguration() + { + var global = GlobalConfigurationFactory(); + var options = FileCacheOptionsFactory(); + + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(options, global, null, null); + + result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); + result.Region.ShouldBe(options.Region); + result.Header.ShouldBe(options.Header); + result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); + } + + [Fact] + public void ShouldCreateCacheOptionsWithDefaults() + { + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(new FileCacheOptions(), null, "/", new List { "GET" }); + + result.TtlSeconds.ShouldBe(0); + result.Region.ShouldBe("GET"); + result.Header.ShouldBe(null); + result.EnableContentHashing.ShouldBe(false); + } + + [Fact] + public void ShouldComputeRegionIfNotProvided() + { + var global = GlobalConfigurationFactory(); + var options = FileCacheOptionsFactory(); + + global.CacheOptions.Region = null; + options.Region = null; + + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(options, global, "/api/values", new List { "GET", "POST" }); + + result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); + result.Region.ShouldBe("GETPOSTapivalues"); + result.Header.ShouldBe(options.Header); + result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); + } + + private static FileGlobalConfiguration GlobalConfigurationFactory() => new() + { + CacheOptions = new FileCacheOptions + { + TtlSeconds = 20, + Region = "globalRegion", + Header = "globalHeader", + EnableContentHashing = false, + }, + }; + + private static FileCacheOptions FileCacheOptionsFactory() => new() + { + TtlSeconds = 10, + Region = "region", + Header = "header", + EnableContentHashing = true, + }; +} diff --git a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs index 299eb30ff..dd0b22bd3 100644 --- a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.UnitTests.Configuration.ChangeTracking { - public class OcelotConfigurationChangeTokenSourceTests + public class OcelotConfigurationChangeTokenSourceTests : UnitTest { private readonly IOcelotConfigurationChangeTokenSource _source; diff --git a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs index 58b20d7ea..20602553e 100644 --- a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.UnitTests.Configuration.ChangeTracking { - public class OcelotConfigurationChangeTokenTests + public class OcelotConfigurationChangeTokenTests : UnitTest { [Fact] public void should_call_callback_with_state() diff --git a/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs index 6b46b0fff..553a32e89 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ClaimToThingConfigurationParserTests + public class ClaimToThingConfigurationParserTests : UnitTest { private Dictionary _dictionary; private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; diff --git a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs index 70438aed8..878d1da77 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ClaimsToThingCreatorTests + public class ClaimsToThingCreatorTests : UnitTest { private readonly Mock _configParser; private Dictionary _claimsToThings; diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs index 2af3224df..7b3f98a2a 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ConfigurationCreatorTests + public class ConfigurationCreatorTests : UnitTest { private ConfigurationCreator _creator; private InternalConfiguration _result; @@ -15,6 +15,7 @@ public class ConfigurationCreatorTests private readonly Mock _hhoCreator; private readonly Mock _lboCreator; private readonly Mock _vCreator; + private readonly Mock _versionPolicyCreator; private FileConfiguration _fileConfig; private List _routes; private ServiceProviderConfiguration _spc; @@ -27,6 +28,7 @@ public class ConfigurationCreatorTests public ConfigurationCreatorTests() { _vCreator = new Mock(); + _versionPolicyCreator = new Mock(); _lboCreator = new Mock(); _hhoCreator = new Mock(); _qosCreator = new Mock(); @@ -114,7 +116,7 @@ private void GivenTheDependenciesAreSetUp() private void WhenICreate() { var serviceProvider = _serviceCollection.BuildServiceProvider(); - _creator = new ConfigurationCreator(_spcCreator.Object, _qosCreator.Object, _hhoCreator.Object, serviceProvider, _lboCreator.Object, _vCreator.Object); + _creator = new ConfigurationCreator(_spcCreator.Object, _qosCreator.Object, _hhoCreator.Object, serviceProvider, _lboCreator.Object, _vCreator.Object, _versionPolicyCreator.Object); _result = _creator.Create(_fileConfig, _routes); } } diff --git a/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs new file mode 100644 index 000000000..b150c6f3c --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs @@ -0,0 +1,118 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +namespace Ocelot.UnitTests.Configuration; + +[Trait("Feat", "738")] +public class DefaultMetadataCreatorTests : UnitTest +{ + private FileGlobalConfiguration _globalConfiguration; + private Dictionary _metadataInRoute; + private MetadataOptions _result; + private readonly DefaultMetadataCreator _sut = new(); + + [Fact] + public void Should_return_empty_metadata() + { + // Arrange + GivenEmptyMetadataInGlobalConfiguration(); + GivenEmptyMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamRouteMetadataMustBeEmpty(); + } + + [Fact] + public void Should_return_global_metadata() + { + // Arrange + GivenSomeMetadataInGlobalConfiguration(); + GivenEmptyMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamMetadataMustContain("foo", "bar"); + } + + [Fact] + public void Should_return_route_metadata() + { + // Arrange + GivenEmptyMetadataInGlobalConfiguration(); + GivenSomeMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamMetadataMustContain("foo", "baz"); + } + + [Fact] + public void Should_overwrite_global_metadata() + { + // Arrange + GivenSomeMetadataInGlobalConfiguration(); + GivenSomeMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamMetadataMustContain("foo", "baz"); + } + + private void WhenICreate() + { + _result = _sut.Create( _metadataInRoute, _globalConfiguration); + } + + private void GivenEmptyMetadataInGlobalConfiguration() + { + _globalConfiguration = new FileGlobalConfiguration(); + } + + private void GivenSomeMetadataInGlobalConfiguration() + { + _globalConfiguration = new FileGlobalConfiguration + { + MetadataOptions = new FileMetadataOptions + { + Metadata = new Dictionary + { + ["foo"] = "bar", + }, + }, + }; + } + + private void GivenEmptyMetadataInRoute() + { + _metadataInRoute = new Dictionary(); + } + + private void GivenSomeMetadataInRoute() + { + _metadataInRoute = new Dictionary + { + ["foo"] = "baz", + }; + } + + private void ThenDownstreamRouteMetadataMustBeEmpty() + { + _result.Metadata.Keys.ShouldBeEmpty(); + } + + private void ThenDownstreamMetadataMustContain(string key, string value) + { + _result.Metadata.Keys.ShouldContain(key); + _result.Metadata[key].ShouldBeEquivalentTo(value); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs index 30a6354a1..eea4ee955 100644 --- a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs @@ -229,7 +229,7 @@ private static FileConfiguration FakeFileConfigurationForGet() private static FileRoute GivenRoute(string host, string downstream) => new() { - DownstreamHostAndPorts = [new(host, 80)], + DownstreamHostAndPorts = new() { new(host, 80) }, DownstreamScheme = Uri.UriSchemeHttps, DownstreamPathTemplate = downstream, }; diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs index 6cb6ece1c..0fe22959f 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class DownstreamAddressesCreatorTests + public class DownstreamAddressesCreatorTests : UnitTest { public DownstreamAddressesCreator _creator; private FileRoute _route; diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs new file mode 100644 index 000000000..0c3b3bcc3 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -0,0 +1,281 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Metadata; +using Ocelot.Values; +using System.Text.Json; + +namespace Ocelot.UnitTests.Configuration; + +[Trait("Feat", "738")] +public class DownstreamRouteExtensionsTests +{ + private readonly DownstreamRoute _downstreamRoute; + + public DownstreamRouteExtensionsTests() + { + _downstreamRoute = new DownstreamRoute( + null, + new UpstreamPathTemplate(null, 0, false, null), + new List(), + new List(), + new List(), + null, + null, + new HttpHandlerOptions(false, false, false, false, 0, TimeSpan.Zero), + default, + default, + new QoSOptions(0, 0, 0, null), + null, + null, + default, + new CacheOptions(0, null, null, null), + new LoadBalancerOptions(null, null, 0), + new RateLimitOptions(false, null, null, false, null, null, null, 0), + new Dictionary(), + new List(), + new List(), + new List(), + new List(), + default, + default, + new AuthenticationOptions(null, null, null), + new DownstreamPathTemplate(null), + null, + new List(), + new List(), + new List(), + default, + new SecurityOptions(), + null, + new Version(), + HttpVersionPolicy.RequestVersionExact, + new(), + new MetadataOptions(new FileMetadataOptions())); + } + + [Theory] + [InlineData("key1", null)] + [InlineData("hello", "world")] + public void Should_return_default_value_when_key_not_found(string key, string defaultValue) + { + // Arrange + _downstreamRoute.MetadataOptions.Metadata.Add(key, defaultValue); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key, defaultValue); + + // Assert + metadataValue.ShouldBe(defaultValue); + } + + [Theory] + [InlineData("hello", "world")] + [InlineData("object.key", "value1,value2,value3")] + public void Should_return_found_metadata_value(string key, string value) + { + // Arrange + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key); + + //Assert + metadataValue.ShouldBe(value); + } + + [Theory] + [InlineData("mykey", "")] + [InlineData("mykey", "value1", "value1")] + [InlineData("mykey", "value1,value2", "value1", "value2")] + [InlineData("mykey", "value1, value2", "value1", "value2")] + [InlineData("mykey", "value1,,,value2", "value1", "value2")] + [InlineData("mykey", "value1, ,value2", "value1", "value2")] + [InlineData("mykey", "value1, value2, value3", "value1", "value2", "value3")] + [InlineData("mykey", ", ,value1, ,, ,,,,,value2,,, ", "value1", "value2")] + public void Should_split_strings(string key, string value, params string[] expected) + { + // Arrange + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key); + + //Assert + metadataValue.ShouldBe(expected); + } + + [Fact] + public void Should_parse_from_json_null() => Should_parse_object_from_json("mykey", "null", null); + + [Fact] + public void Should_parse_from_json_string() => Should_parse_object_from_json("mykey", "string", "string"); + + [Fact] + public void Should_parse_from_json_numbers() => Should_parse_object_from_json("mykey", "123", 123); + + [Fact] + public void Should_parse_from_object() + => Should_parse_object_from_json( + "mykey", + "{\"Id\": 88, \"Value\": \"Hello World!\", \"MyTime\": \"2024-01-01T10:10:10.000Z\"}", + new FakeObject { Id = 88, Value = "Hello World!", MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified) }); + + private void Should_parse_object_from_json(string key, string value, object expected) + { + // Arrange + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key); + + //Assert + metadataValue.ShouldBeEquivalentTo(expected); + } + + [Fact] + public void Should_parse_from_json_array() + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, "[\"value1\", \"value2\", \"value3\"]"); + + // Act + var metadataValue = _downstreamRoute.GetMetadata>(key); + + //Assert + IEnumerable enumerable = metadataValue as string[] ?? metadataValue.ToArray(); + enumerable.ShouldNotBeNull(); + enumerable.ElementAt(0).ShouldBe("value1"); + enumerable.ElementAt(1).ShouldBe("value2"); + enumerable.ElementAt(2).ShouldBe("value3"); + } + + [Fact] + public void Should_throw_error_when_invalid_json() + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, "[[["); + + // Act + + //Assert + Assert.Throws(() => + { + _ = _downstreamRoute.GetMetadata>(key); + }); + } + + [Fact] + public void Should_parse_json_with_custom_json_settings_options() + { + // Arrange + var key = "mykey"; + var value = "{\"id\": 88, \"value\": \"Hello World!\", \"myTime\": \"2024-01-01T10:10:10.000Z\"}"; + var expected = new FakeObject + { + Id = 88, + Value = "Hello World!", + MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified), + }; + var serializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key, jsonSerializerOptions: serializerOptions); + + //Assert + metadataValue.ShouldBeEquivalentTo(expected); + } + + record FakeObject + { + public int Id { get; set; } + public string Value { get; set; } + public DateTime MyTime { get; set; } + } + + [Theory] + [InlineData("0", 0)] + [InlineData("99", 99)] + [InlineData("500", 500)] + [InlineData("999999999", 999999999)] + public void Should_parse_integers(string value, int expected) => Should_parse_number(value, expected); + + [Theory] + [InlineData("0", 0)] + [InlineData("0.5", 0.5)] + [InlineData("99", 99)] + [InlineData("99.5", 99.5)] + [InlineData("999999999", 999999999)] + [InlineData("999999999.5", 999999999.5)] + public void Should_parse_double(string value, double expected) => Should_parse_number(value, expected); + + private void Should_parse_number(string value, T expected) + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key); + + //Assert + metadataValue.ShouldBe(expected); + } + + [Fact] + public void Should_throw_error_when_invalid_number() + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, "xyz"); + + // Act + + // Assert + Assert.Throws(() => + { + _ = _downstreamRoute.GetMetadata(key); + }); + } + + [Theory] + [InlineData("true", true)] + [InlineData("yes", true)] + [InlineData("on", true)] + [InlineData("enabled", true)] + [InlineData("enable", true)] + [InlineData("ok", true)] + [InlineData(" true ", true)] + [InlineData(" yes ", true)] + [InlineData(" on ", true)] + [InlineData(" enabled ", true)] + [InlineData(" enable ", true)] + [InlineData(" ok ", true)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData(null, false)] + [InlineData("false", false)] + [InlineData("off", false)] + [InlineData("disabled", false)] + [InlineData("disable", false)] + [InlineData("no", false)] + [InlineData("abcxyz", false)] + public void Should_parse_truthy_values(string value, bool expected) + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var isTrusthy = _downstreamRoute.GetMetadata(key); + + //Assert + isTrusthy.ShouldBe(expected); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index 1a8b31566..3d06802fc 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -5,74 +5,89 @@ namespace Ocelot.UnitTests.Configuration { - public class DynamicsCreatorTests + 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 List _result; private FileConfiguration _fileConfig; private RateLimitOptions _rlo1; private RateLimitOptions _rlo2; 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); + _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object, _metadataCreator.Object); } [Fact] - public void should_return_nothing() + public void Should_return_nothing() { + // Arrange var fileConfig = new FileConfiguration(); + GivenThe(fileConfig); - this.Given(_ => GivenThe(fileConfig)) - .When(_ => WhenICreate()) - .Then(_ => ThenNothingIsReturned()) - .And(_ => ThenTheRloCreatorIsNotCalled()) - .BDDfy(); + // Act + WhenICreate(); + + // Assert + ThenNothingIsReturned(); + ThenTheRloCreatorIsNotCalled(); + ThenTheMetadataCreatorIsNotCalled(); } [Fact] - public void should_return_re_routes() + public void Should_return_routes() { + // Arrange var fileConfig = new FileConfiguration { - DynamicRoutes = new List + DynamicRoutes = new() { - new() - { - ServiceName = "1", - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = false, - }, - DownstreamHttpVersion = "1.1", - }, - new() - { - ServiceName = "2", - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = true, - }, - DownstreamHttpVersion = "2.0", - }, + GivenDynamicRoute("1", false, "1.1", "foo", "bar"), + GivenDynamicRoute("2", true, "2.0", "foo", "baz"), }, }; - - this.Given(_ => GivenThe(fileConfig)) - .And(_ => GivenTheRloCreatorReturns()) - .And(_ => GivenTheVersionCreatorReturns()) - .When(_ => WhenICreate()) - .Then(_ => ThenTheRoutesAreReturned()) - .And(_ => ThenTheRloCreatorIsCalledCorrectly()) - .And(_ => ThenTheVersionCreatorIsCalledCorrectly()) - .BDDfy(); + GivenThe(fileConfig); + GivenTheRloCreatorReturns(); + GivenTheVersionCreatorReturns(); + GivenTheVersionPolicyCreatorReturns(); + GivenTheMetadataCreatorReturns(); + + // Act + WhenICreate(); + + // Assert + ThenTheRoutesAreReturned(); + ThenTheRloCreatorIsCalledCorrectly(); + ThenTheVersionCreatorIsCalledCorrectly(); + ThenTheMetadataCreatorIsCalledCorrectly(); } + private FileDynamicRoute GivenDynamicRoute(string serviceName, bool enableRateLimiting, + string downstreamHttpVersion, string key, string value) => new() + { + ServiceName = serviceName, + RateLimitRule = new FileRateLimitRule + { + EnableRateLimiting = enableRateLimiting, + }, + DownstreamHttpVersion = downstreamHttpVersion, + Metadata = new Dictionary + { + [key] = value, + }, + }; + private void ThenTheRloCreatorIsCalledCorrectly() { _rloCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].RateLimitRule, @@ -86,6 +101,15 @@ 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)); + } + + 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() @@ -94,11 +118,13 @@ private void ThenTheRoutesAreReturned() _result[0].DownstreamRoute[0].EnableEndpointEndpointRateLimiting.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].EnableEndpointEndpointRateLimiting.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); } @@ -106,6 +132,22 @@ private void GivenTheVersionCreatorReturns() { _version = new Version("1.1"); _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_version); + } + + private void GivenTheVersionPolicyCreatorReturns() + { + _versionPolicy = HttpVersionPolicy.RequestVersionOrLower; + _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_versionPolicy); + } + + private void GivenTheMetadataCreatorReturns() + { + _expectedMetadata = new() + { + ["foo"] = "bar", + }; + _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) + .Returns(new MetadataOptions(new FileMetadataOptions{Metadata = _expectedMetadata})); } private void GivenTheRloCreatorReturns() @@ -123,6 +165,11 @@ private void ThenTheRloCreatorIsNotCalled() { _rloCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); } + + private void ThenTheMetadataCreatorIsNotCalled() + { + _metadataCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Never); + } private void ThenNothingIsReturned() { diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs index 524a66dba..366f40ae8 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration { - public class FileConfigurationPollerTests : IDisposable + public class FileConfigurationPollerTests : UnitTest, IDisposable { private readonly FileConfigurationPoller _poller; private readonly Mock _factory; diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs index ba4df0d0f..74709d949 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration { - public class FileConfigurationSetterTests + public class FileConfigurationSetterTests : UnitTest { private FileConfiguration _fileConfiguration; private readonly FileAndInternalConfigurationSetter _configSetter; @@ -32,7 +32,17 @@ public void should_set_configuration() { var fileConfig = new FileConfiguration(); var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - var config = new InternalConfiguration(new List(), string.Empty, serviceProviderConfig, "asdf", new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + var config = new InternalConfiguration( + new List(), + string.Empty, + serviceProviderConfig, + "asdf", + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(x => GivenTheFollowingConfiguration(fileConfig)) .And(x => GivenTheRepoReturns(new OkResponse())) diff --git a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index 865604071..802a4588e 100644 --- a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration { - public class FileInternalConfigurationCreatorTests + public class FileInternalConfigurationCreatorTests : UnitTest { private readonly Mock _validator; private readonly Mock _routesCreator; @@ -79,7 +79,7 @@ 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); + _internalConfig = new InternalConfiguration(null, string.Empty, null, string.Empty, null, string.Empty, null, null, null, null); _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/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index 854f2abea..b5f42d6e3 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Configuration { - public class HeaderFindAndReplaceCreatorTests + public class HeaderFindAndReplaceCreatorTests : UnitTest { private readonly HeaderFindAndReplaceCreator _creator; private FileRoute _route; diff --git a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs index 8e41eec9e..33f6d629a 100644 --- a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration { - public class HttpHandlerOptionsCreatorTests + public class HttpHandlerOptionsCreatorTests : UnitTest { private IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private FileRoute _fileRoute; diff --git a/test/Ocelot.UnitTests/Configuration/HttpVersionPolicyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpVersionPolicyCreatorTests.cs new file mode 100644 index 000000000..813752d60 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/HttpVersionPolicyCreatorTests.cs @@ -0,0 +1,50 @@ +using Ocelot.Configuration.Creator; + +namespace Ocelot.UnitTests.Configuration; + +[Trait("Feat", "1672")] +public sealed class HttpVersionPolicyCreatorTests : UnitTest +{ + private readonly HttpVersionPolicyCreator _creator; + + public HttpVersionPolicyCreatorTests() + { + _creator = new HttpVersionPolicyCreator(); + } + + [Theory] + [InlineData(VersionPolicies.RequestVersionOrLower, HttpVersionPolicy.RequestVersionOrLower)] + [InlineData(VersionPolicies.RequestVersionExact, HttpVersionPolicy.RequestVersionExact)] + [InlineData(VersionPolicies.RequestVersionOrHigher, HttpVersionPolicy.RequestVersionOrHigher)] + public void Should_create_version_policy_based_on_input(string versionPolicy, HttpVersionPolicy expected) + { + // Arrange, Act + var actual = _creator.Create(versionPolicy); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("invalid version")] + public void Should_default_to_request_version_or_lower(string versionPolicy) + { + // Arrange, Act + var actual = _creator.Create(versionPolicy); + + // Assert + Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, actual); + } + + [Fact] + public void Should_default_to_request_version_or_lower_when_setting_gibberish() + { + // Arrange, Act + var actual = _creator.Create("string is gibberish"); + + // Assert + Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, actual); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs index a9af24523..e6f502438 100644 --- a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -3,10 +3,10 @@ using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.Repository; using Ocelot.Responses; - + namespace Ocelot.UnitTests.Configuration { - public class InMemoryConfigurationRepositoryTests + public class InMemoryConfigurationRepositoryTests : UnitTest { private readonly InMemoryInternalConfigurationRepository _repo; private IInternalConfiguration _config; @@ -115,6 +115,7 @@ public List Routes public QoSOptions QoSOptions { get; } public HttpHandlerOptions HttpHandlerOptions { get; } public Version DownstreamHttpVersion { get; } + public HttpVersionPolicy? DownstreamHttpVersionPolicy { get; } } } } diff --git a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs index d7d2e1bac..7e1464ecd 100644 --- a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class LoadBalancerOptionsCreatorTests + public class LoadBalancerOptionsCreatorTests : UnitTest { private readonly ILoadBalancerOptionsCreator _creator; private FileLoadBalancerOptions _fileLoadBalancerOptions; diff --git a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs index 893c8630b..f394932df 100644 --- a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class QoSOptionsCreatorTests + public class QoSOptionsCreatorTests : UnitTest { private readonly QoSOptionsCreator _creator; private FileRoute _fileRoute; diff --git a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs index 63e779312..e514877d2 100644 --- a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RateLimitOptionsCreatorTests + public class RateLimitOptionsCreatorTests : UnitTest { private FileRoute _fileRoute; private FileGlobalConfiguration _fileGlobalConfig; diff --git a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs index 4511c04fa..d2be4fcc5 100644 --- a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RequestIdKeyCreatorTests + public class RequestIdKeyCreatorTests : UnitTest { private FileRoute _fileRoute; private FileGlobalConfiguration _fileGlobalConfig; diff --git a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs index 8326fb5a0..ff26b406f 100644 --- a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RouteKeyCreatorTests + public class RouteKeyCreatorTests : UnitTest { private readonly RouteKeyCreator _creator; private FileRoute _route; @@ -39,12 +39,12 @@ public void Should_return_route_key() var route = new FileRoute { UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = ["GET", "POST", "PUT"], - DownstreamHostAndPorts = - [ + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, + DownstreamHostAndPorts = new() + { new("localhost", 8080), new("localhost", 4430), - ], + }, }; this.Given(_ => GivenThe(route)) @@ -60,12 +60,12 @@ public void Should_return_route_key_with_upstream_host() { UpstreamHost = "my-host", UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = ["GET", "POST", "PUT"], - DownstreamHostAndPorts = - [ + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, + DownstreamHostAndPorts = new() + { new("localhost", 8080), new("localhost", 4430), - ], + }, }; this.Given(_ => GivenThe(route)) @@ -80,7 +80,7 @@ public void Should_return_route_key_with_svc_name() var route = new FileRoute { UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = ["GET", "POST", "PUT"], + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, ServiceName = "products-service", }; @@ -96,7 +96,7 @@ public void Should_return_route_key_with_load_balancer_options() var route = new FileRoute { UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = ["GET", "POST", "PUT"], + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, ServiceName = "products-service", LoadBalancerOptions = new FileLoadBalancerOptions { diff --git a/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs index bfb2927e5..21f9058ae 100644 --- a/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration; -public class RouteOptionsCreatorTests +public class RouteOptionsCreatorTests : UnitTest { private readonly RouteOptionsCreator _creator; @@ -126,7 +126,7 @@ public void Create_RouteOptions_HappyPath(bool isAuthenticationProviderKeys) { AuthenticationProviderKey = !isAuthenticationProviderKeys ? "Test" : null, AuthenticationProviderKeys = isAuthenticationProviderKeys ? - [string.Empty, "Test #1"] : null, + new string[] { string.Empty, "Test #1" } : null, }, RouteClaimsRequirement = new Dictionary { diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index cf5f38e7b..dbb9ef25a 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -1,5 +1,4 @@ -using Ocelot.Cache; -using Ocelot.Configuration; +using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; @@ -7,17 +6,18 @@ namespace Ocelot.UnitTests.Configuration { - public class RoutesCreatorTests + 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 _rCreator; + private readonly Mock _coCreator; private readonly Mock _hhoCreator; private readonly Mock _hfarCreator; private readonly Mock _daCreator; @@ -25,6 +25,8 @@ public class RoutesCreatorTests 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; @@ -34,13 +36,16 @@ public class RoutesCreatorTests private List _ctt; private QoSOptions _qoso; private RateLimitOptions _rlo; - private string _region; + private CacheOptions _cacheOptions; private HttpHandlerOptions _hho; private HeaderTransformations _ht; private List _dhp; private LoadBalancerOptions _lbo; private List _result; - private Version _expectedVersion; + private Version _expectedVersion; + private HttpVersionPolicy _expectedVersionPolicy; + private Dictionary _uht; + private Dictionary _expectedMetadata; public RoutesCreatorTests() { @@ -51,7 +56,7 @@ public RoutesCreatorTests() _qosoCreator = new Mock(); _rroCreator = new Mock(); _rloCreator = new Mock(); - _rCreator = new Mock(); + _coCreator = new Mock(); _hhoCreator = new Mock(); _hfarCreator = new Mock(); _daCreator = new Mock(); @@ -59,6 +64,9 @@ public RoutesCreatorTests() _rrkCreator = new Mock(); _soCreator = new Mock(); _versionCreator = new Mock(); + _versionPolicyCreator = new Mock(); + _uhtpCreator = new Mock(); + _metadataCreator = new Mock(); _creator = new RoutesCreator( _cthCreator.Object, @@ -68,19 +76,21 @@ public RoutesCreatorTests() _qosoCreator.Object, _rroCreator.Object, _rloCreator.Object, - _rCreator.Object, + _coCreator.Object, _hhoCreator.Object, _hfarCreator.Object, _daCreator.Object, _lboCreator.Object, _rrkCreator.Object, _soCreator.Object, - _versionCreator.Object - ); + _versionCreator.Object, + _versionPolicyCreator.Object, + _uhtpCreator.Object, + _metadataCreator.Object); } [Fact] - public void should_return_nothing() + public void Should_return_nothing() { var fileConfig = new FileConfiguration(); @@ -91,7 +101,7 @@ public void should_return_nothing() } [Fact] - public void should_return_re_routes() + public void Should_return_routes() { var fileConfig = new FileConfiguration { @@ -113,7 +123,11 @@ public void should_return_re_routes() { { "e","f" }, }, - UpstreamHttpMethod = new List { "GET", "POST" }, + UpstreamHttpMethod = new List { "GET", "POST" }, + Metadata = new Dictionary + { + ["foo"] = "bar", + }, }, new() { @@ -132,6 +146,10 @@ public void should_return_re_routes() { "k","l" }, }, UpstreamHttpMethod = new List { "PUT", "DELETE" }, + Metadata = new Dictionary + { + ["foo"] = "baz", + }, }, }, }; @@ -153,6 +171,7 @@ private void ThenTheDependenciesAreCalledCorrectly() private void GivenTheDependenciesAreSetUpCorrectly() { _expectedVersion = new Version("1.1"); + _expectedVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; _rro = new RouteOptions(false, false, false, false, false); _requestId = "testy"; _rrk = "besty"; @@ -161,11 +180,17 @@ private void GivenTheDependenciesAreSetUpCorrectly() _ctt = new List(); _qoso = new QoSOptionsBuilder().Build(); _rlo = new RateLimitOptionsBuilder().Build(); - _region = "vesty"; + + _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(); + _expectedMetadata = new Dictionary() + { + ["foo"] = "bar", + }; _rroCreator.Setup(x => x.Create(It.IsAny())).Returns(_rro); _ridkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_requestId); @@ -175,12 +200,18 @@ private void GivenTheDependenciesAreSetUpCorrectly() _cthCreator.Setup(x => x.Create(It.IsAny>())).Returns(_ctt); _qosoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())).Returns(_qoso); _rloCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rlo); - _rCreator.Setup(x => x.Create(It.IsAny())).Returns(_region); + _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); _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); + _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() @@ -209,6 +240,7 @@ private void WhenICreate() 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); @@ -222,8 +254,8 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) _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(_region); - _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(expected.FileCacheOptions.TtlSeconds); + _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); @@ -239,6 +271,7 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) _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() @@ -250,6 +283,7 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) _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) @@ -264,12 +298,13 @@ private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguratio _cthCreator.Verify(x => x.Create(fileRoute.AddQueriesToRequest), Times.Once); _qosoCreator.Verify(x => x.Create(fileRoute.QoSOptions, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod)); _rloCreator.Verify(x => x.Create(fileRoute.RateLimitOptions, globalConfig), Times.Once); - _rCreator.Verify(x => x.Create(fileRoute), 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.Once); _daCreator.Verify(x => x.Create(fileRoute), Times.Once); _lboCreator.Verify(x => x.Create(fileRoute.LoadBalancerOptions), Times.Once); - _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions), Times.Once); + _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions), Times.Once); + _metadataCreator.Verify(x => x.Create(fileRoute.Metadata, globalConfig), Times.Once); } } } diff --git a/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs index 6c2968020..81d9fd36d 100644 --- a/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class SecurityOptionsCreatorTests + public class SecurityOptionsCreatorTests : UnitTest { private FileRoute _fileRoute; private SecurityOptions _result; diff --git a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs index ac92db206..aaa238cca 100644 --- a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ServiceProviderCreatorTests + public class ServiceProviderCreatorTests : UnitTest { private readonly ServiceProviderConfigurationCreator _creator; private FileGlobalConfiguration _globalConfig; diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamHeaderTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamHeaderTemplatePatternCreatorTests.cs new file mode 100644 index 000000000..49cf841ac --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/UpstreamHeaderTemplatePatternCreatorTests.cs @@ -0,0 +1,45 @@ +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Values; + +namespace Ocelot.UnitTests.Configuration; + +public class UpstreamHeaderTemplatePatternCreatorTests +{ + private readonly UpstreamHeaderTemplatePatternCreator _creator; + + public UpstreamHeaderTemplatePatternCreatorTests() + { + _creator = new(); + } + + [Trait("PR", "1312")] + [Trait("Feat", "360")] + [Theory(DisplayName = "Should create pattern")] + [InlineData("country", "a text without placeholders", "^(?i)a text without placeholders$", " without placeholders")] + [InlineData("country", "a text without placeholders", "^a text without placeholders$", " Route is case sensitive", true)] + [InlineData("country", "{header:start}rest of the text", "^(?i)(?.+)rest of the text$", " with placeholder in the beginning")] + [InlineData("country", "rest of the text{header:end}", "^(?i)rest of the text(?.+)$", " with placeholder at the end")] + [InlineData("country", "{header:countrycode}", "^(?i)(?.+)$", " with placeholder only")] + [InlineData("country", "any text {header:cc} and other {header:version} and {header:bob} the end", "^(?i)any text (?.+) and other (?.+) and (?.+) the end$", " with more placeholders")] + public void Create_WithUpstreamHeaderTemplates_ShouldCreatePattern(string key, string template, string expected, string withMessage, bool? isCaseSensitive = null) + { + // Arrange + var fileRoute = new FileRoute + { + RouteIsCaseSensitive = isCaseSensitive ?? false, + UpstreamHeaderTemplates = new Dictionary + { + [key] = template, + }, + }; + + // Act + var actual = _creator.Create(fileRoute); + + // Assert + var message = nameof(Create_WithUpstreamHeaderTemplates_ShouldCreatePattern).Replace('_', ' ') + withMessage; + actual[key].ShouldNotBeNull() + .Template.ShouldBe(expected, message); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs index 4028d456b..76cb789d3 100644 --- a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class UpstreamTemplatePatternCreatorTests + public class UpstreamTemplatePatternCreatorTests : UnitTest { private FileRoute _fileRoute; private readonly UpstreamTemplatePatternCreator _creator; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index c45cb9d54..54b61aed0 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -18,7 +18,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class FileConfigurationFluentValidatorTests + public class FileConfigurationFluentValidatorTests : UnitTest { private IConfigurationValidator _configurationValidator; private FileConfiguration _fileConfiguration; @@ -33,7 +33,7 @@ public FileConfigurationFluentValidatorTests() _authProvider = new Mock(); _provider = _services.BuildServiceProvider(); - // Todo - replace with mocks + // TODO Replace with mocks _configurationValidator = new FileConfigurationFluentValidator(_provider, new RouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(_provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(_provider))); } @@ -201,19 +201,15 @@ public void Configuration_is_valid_if_aggregates_are_valid() var route2 = GivenDefaultRoute("/tom", "/"); route2.Key = "Tom"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -229,19 +225,15 @@ public void Configuration_is_invalid_if_aggregates_are_duplicate_of_routes() route2.Key = "Tom"; route2.UpstreamHost = "localhost"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -256,21 +248,17 @@ public void Configuration_is_valid_if_aggregates_are_not_duplicate_of_routes() route.Key = "Laura"; var route2 = GivenDefaultRoute("/tom", "/"); route2.Key = "Tom"; - route2.UpstreamHttpMethod = ["Post"]; + route2.UpstreamHttpMethod = new() { "Post" }; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -285,29 +273,21 @@ public void Configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() var route2 = GivenDefaultRoute("/lol", "/"); route2.Key = "Tom"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -321,19 +301,15 @@ public void Configuration_is_invalid_if_routes_dont_exist_for_aggregate() var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; var configuration = GivenAConfiguration(route); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -350,19 +326,15 @@ public void Configuration_is_invalid_if_aggregate_has_routes_with_specific_reque route2.Key = "Tom"; route2.RequestIdKey = "should_fail"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -455,7 +427,7 @@ public void Configuration_is_invalid_with_invalid_authentication_provider() route.AuthenticationOptions = new FileAuthenticationOptions() { AuthenticationProviderKey = "Test", - AuthenticationProviderKeys = ["Test #1", "Test #2"], + AuthenticationProviderKeys = new string[] { "Test #1", "Test #2" }, }; this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) @@ -495,7 +467,7 @@ public void Configuration_is_not_valid_with_duplicate_routes_specific_verbs() { var route = GivenDefaultRoute(); var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = ["Get"]; + duplicate.UpstreamHttpMethod = new() { "Get" }; this.Given(x => x.GivenAConfiguration(route, duplicate)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -508,7 +480,7 @@ public void Configuration_is_valid_with_duplicate_routes_different_verbs() { var route = GivenDefaultRoute(); // "Get" verb is inside var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = ["Post"]; + duplicate.UpstreamHttpMethod = new() { "Post" }; this.Given(x => x.GivenAConfiguration(route, duplicate)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -519,11 +491,11 @@ public void Configuration_is_valid_with_duplicate_routes_different_verbs() public void Configuration_is_not_valid_with_duplicate_routes_with_duplicated_upstreamhosts() { var route = GivenDefaultRoute(); - route.UpstreamHttpMethod = []; + route.UpstreamHttpMethod = new(); route.UpstreamHost = "upstreamhost"; var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = []; + duplicate.UpstreamHttpMethod = new(); duplicate.UpstreamHost = "upstreamhost"; this.Given(x => x.GivenAConfiguration(route, duplicate)) @@ -537,11 +509,11 @@ public void Configuration_is_not_valid_with_duplicate_routes_with_duplicated_ups public void Configuration_is_valid_with_duplicate_routes_but_different_upstreamhosts() { var route = GivenDefaultRoute(); - route.UpstreamHttpMethod = []; + route.UpstreamHttpMethod = new(); route.UpstreamHost = "upstreamhost111"; var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = []; + duplicate.UpstreamHttpMethod = new(); duplicate.UpstreamHost = "upstreamhost222"; this.Given(x => x.GivenAConfiguration(route, duplicate)) @@ -554,17 +526,17 @@ public void Configuration_is_valid_with_duplicate_routes_but_different_upstreamh public void Configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is_not_set() { var route = GivenDefaultRoute(); - route.UpstreamHttpMethod = []; + route.UpstreamHttpMethod = new(); route.UpstreamHost = "upstreamhost"; var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = []; + duplicate.UpstreamHttpMethod = new(); this.Given(x => x.GivenAConfiguration(route, duplicate)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) .BDDfy(); - } + } [Fact] public void Configuration_is_invalid_with_invalid_rate_limit_configuration() @@ -697,10 +669,10 @@ public void HaveServiceDiscoveryProviderRegistered_ServiceDiscoveryFinderDelegat public void Configuration_is_valid_when_not_using_service_discovery_and_host_is_set() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = - [ + route.DownstreamHostAndPorts = new() + { new("bbc.co.uk", 123), - ]; + }; this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -711,10 +683,10 @@ public void Configuration_is_valid_when_not_using_service_discovery_and_host_is_ public void Configuration_is_valid_when_no_downstream_but_has_host_and_port() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = - [ + route.DownstreamHostAndPorts = new() + { new("test", 123), - ]; + }; this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -725,7 +697,7 @@ public void Configuration_is_valid_when_no_downstream_but_has_host_and_port() public void Configuration_is_not_valid_when_no_host_and_port() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = []; + route.DownstreamHostAndPorts = new(); this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -737,15 +709,111 @@ public void Configuration_is_not_valid_when_no_host_and_port() public void Configuration_is_not_valid_when_host_and_port_is_empty() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = - [ + route.DownstreamHostAndPorts = new() + { new(), - ]; + }; this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!")) .BDDfy(); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Configuration_is_not_valid_when_upstream_headers_the_same() + { + // Arrange + var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() + { + { "header1", "value1" }, + { "header2", "value2" }, + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new() + { + { "header2", "value2" }, + { "header1", "value1" }, + }); + GivenAConfiguration(route1, route2); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenTheResultIsNotValid(); + ThenTheErrorMessageAtPositionIs(0, "route /asdf/ has duplicate"); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Configuration_is_valid_when_upstream_headers_not_the_same() + { + // Arrange + var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() + { + { "header1", "value1" }, + { "header2", "value2" }, + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new() + { + { "header2", "value2" }, + { "header1", "valueDIFFERENT" }, + }); + GivenAConfiguration(route1, route2); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenTheResultIsValid(); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Configuration_is_valid_when_upstream_headers_count_not_the_same() + { + // Arrange + var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() + { + { "header1", "value1" }, + { "header2", "value2" }, + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new() + { + { "header2", "value2" }, + }); + GivenAConfiguration(route1, route2); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenTheResultIsValid(); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Configuration_is_valid_when_one_upstream_headers_empty_and_other_not_empty() + { + // Arrange + var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() + { + { "header1", "value1" }, + { "header2", "value2" }, + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new()); + GivenAConfiguration(route1, route2); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenTheResultIsValid(); } [Theory] @@ -815,23 +883,35 @@ public void Configuration_is_invalid_when_placeholder_is_used_twice_in_downstrea private static FileRoute GivenDefaultRoute(string upstream, string downstream, string host) => new() { - UpstreamHttpMethod = [HttpMethods.Get], + UpstreamHttpMethod = new() { HttpMethods.Get }, UpstreamPathTemplate = upstream ?? "/asdf/", DownstreamPathTemplate = downstream ?? "/api/products/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new(host ?? "bbc.co.uk", 12345), - ], + }, DownstreamScheme = Uri.UriSchemeHttp, }; private static FileRoute GivenServiceDiscoveryRoute() => new() { - UpstreamHttpMethod = [HttpMethods.Get], + UpstreamHttpMethod = new() { HttpMethods.Get }, UpstreamPathTemplate = "/laura", DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, ServiceName = "test", + }; + + private static FileRoute GivenRouteWithUpstreamHeaderTemplates(string upstream, string downstream, Dictionary templates) => new() + { + UpstreamPathTemplate = upstream, + DownstreamPathTemplate = downstream, + DownstreamHostAndPorts = new() + { + new("bbc.co.uk", 123), + }, + UpstreamHttpMethod = new() { HttpMethods.Get }, + UpstreamHeaderTemplates = templates, }; private void GivenAConfiguration(FileConfiguration fileConfiguration) => _fileConfiguration = fileConfiguration; @@ -918,7 +998,7 @@ private void GivenAServiceDiscoveryHandler() private class FakeServiceDiscoveryProvider : IServiceDiscoveryProvider { - public Task> GetAsync() => Task.FromResult>([]); + public Task> GetAsync() => Task.FromResult>(new()); } private class TestOptions : AuthenticationSchemeOptions { } @@ -930,12 +1010,10 @@ private class TestHandler : AuthenticationHandler // It can be set directly or by registering a provider in the dependency injection container. #if NET8_0_OR_GREATER public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) - { - } + { } #else public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) - { - } + { } #endif protected override Task HandleAuthenticateAsync() diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs index 69a7401e9..84d041e23 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class FileQoSOptionsFluentValidatorTests + public class FileQoSOptionsFluentValidatorTests : UnitTest { private FileQoSOptionsFluentValidator _validator; private readonly ServiceCollection _services; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs index 9ea2bb364..af975654f 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class HostAndPortValidatorTests + public class HostAndPortValidatorTests : UnitTest { private HostAndPortValidator _validator; private ValidationResult _result; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs index 2e696f41d..1b2bfadb3 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class RouteFluentValidatorTests + public class RouteFluentValidatorTests : UnitTest { private readonly RouteFluentValidator _validator; private readonly Mock _authProvider; diff --git a/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs index dde40682a..129ff791a 100644 --- a/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.UnitTests.Configuration { - public class VersionCreatorTests + public class VersionCreatorTests : UnitTest { private readonly HttpVersionCreator _creator; private string _input; diff --git a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs index e7f49f3d2..0f8eaf192 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs @@ -5,12 +5,13 @@ using Ocelot.Configuration.File; using Ocelot.Logging; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.Responses; using System.Text; namespace Ocelot.UnitTests.Consul { - public class ConsulFileConfigurationRepositoryTests + public class ConsulFileConfigurationRepositoryTests : UnitTest { private ConsulFileConfigurationRepository _repo; private readonly Mock> _options; diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs deleted file mode 100644 index bf570142c..000000000 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -using Consul; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; -using Ocelot.Logging; -using Ocelot.Provider.Consul; -using Ocelot.Values; -using ConsulProvider = Ocelot.Provider.Consul.Consul; - -namespace Ocelot.UnitTests.Consul -{ - public class ConsulServiceDiscoveryProviderTests : IDisposable - { - private IWebHost _fakeConsulBuilder; - private readonly List _serviceEntries; - private ConsulProvider _provider; - private readonly string _serviceName; - private readonly int _port; - private readonly string _consulHost; - private readonly string _consulScheme; - private readonly string _fakeConsulServiceDiscoveryUrl; - private List _services; - private readonly Mock _factory; - private readonly Mock _logger; - private string _receivedToken; - private readonly IConsulClientFactory _clientFactory; - - public ConsulServiceDiscoveryProviderTests() - { - _serviceName = "test"; - _port = 8500; - _consulHost = "localhost"; - _consulScheme = "http"; - _fakeConsulServiceDiscoveryUrl = $"{_consulScheme}://{_consulHost}:{_port}"; - _serviceEntries = new List(); - _factory = new Mock(); - _clientFactory = new ConsulClientFactory(); - _logger = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, null); - _provider = new ConsulProvider(config, _factory.Object, _clientFactory); - } - - [Fact] - public void should_return_service_from_consul() - { - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = _serviceName, - Address = "localhost", - Port = 50881, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(1)) - .BDDfy(); - } - - [Fact] - public void should_use_token() - { - var token = "test token"; - var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, token); - _provider = new ConsulProvider(config, _factory.Object, _clientFactory); - - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = _serviceName, - Address = "localhost", - Port = 50881, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - this.Given(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .When(_ => WhenIGetTheServices()) - .Then(_ => ThenTheCountIs(1)) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); - } - - [Fact] - public void should_not_return_services_with_invalid_address() - { - var serviceEntryOne = GivenService(address: "http://localhost", port: 50881) - .ToServiceEntry(); - var serviceEntryTwo = GivenService(address: "http://localhost", port: 50888) - .ToServiceEntry(); - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(0)) - .And(x => ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo)) - .BDDfy(); - } - - [Fact] - public void should_not_return_services_with_empty_address() - { - var serviceEntryOne = GivenService(port: 50881) - .WithAddress(string.Empty) - .ToServiceEntry(); - var serviceEntryTwo = GivenService(port: 50888) - .WithAddress(null) - .ToServiceEntry(); - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(0)) - .And(x => ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo)) - .BDDfy(); - } - - [Fact] - public void should_not_return_services_with_invalid_port() - { - var serviceEntryOne = GivenService(port: -1) - .ToServiceEntry(); - var serviceEntryTwo = GivenService(port: 0) - .ToServiceEntry(); - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(0)) - .And(x => ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo)) - .BDDfy(); - } - - private AgentService GivenService(string address = null, int? port = null, string id = null, string[] tags = null) - => new() - { - Service = _serviceName, - Address = address ?? "localhost", - Port = port ?? 123, - ID = id ?? Guid.NewGuid().ToString(), - Tags = tags ?? Array.Empty(), - }; - - private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(params ServiceEntry[] serviceEntries) - { - foreach (var entry in serviceEntries) - { - var service = entry.Service; - var expected = $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."; - _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); - } - } - - private void ThenTheCountIs(int count) - { - _services.Count.ShouldBe(count); - } - - private void WhenIGetTheServices() - { - _services = _provider.GetAsync().GetAwaiter().GetResult(); - } - - private void ThenTheTokenIs(string token) - { - _receivedToken.ShouldBe(token); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _serviceEntries.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _fakeConsulBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) - { - _receivedToken = values.First(); - } - - var json = JsonConvert.SerializeObject(_serviceEntries); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - }) - .Build(); - - _fakeConsulBuilder.Start(); - } - - public void Dispose() - { - _fakeConsulBuilder?.Dispose(); - } - } -} diff --git a/test/Ocelot.UnitTests/Consul/ConsulTests.cs b/test/Ocelot.UnitTests/Consul/ConsulTests.cs new file mode 100644 index 000000000..b9009d488 --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/ConsulTests.cs @@ -0,0 +1,209 @@ +using Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using System.Runtime.CompilerServices; +using ConsulProvider = Ocelot.Provider.Consul.Consul; + +namespace Ocelot.UnitTests.Consul; + +public sealed class ConsulTests : UnitTest, IDisposable +{ + private readonly int _port; + private readonly string _consulHost; + private readonly string _consulScheme; + private readonly string _fakeConsulServiceDiscoveryUrl; + private readonly List _consulServiceEntries; + private readonly Mock _factory; + private readonly Mock _logger; + private IConsulClientFactory _clientFactory; + private IConsulServiceBuilder _serviceBuilder; + private ConsulRegistryConfiguration _config; + private IWebHost _fakeConsulBuilder; + private ConsulProvider _provider; + private string _receivedToken; + + public ConsulTests() + { + _port = 8500; + _consulHost = "localhost"; + _consulScheme = "http"; + _fakeConsulServiceDiscoveryUrl = $"{_consulScheme}://{_consulHost}:{_port}"; + _consulServiceEntries = new List(); + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + } + + public void Dispose() + { + _fakeConsulBuilder?.Dispose(); + } + + private void Arrange([CallerMemberName] string serviceName = null) + { + _config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, serviceName, null); + _clientFactory = new ConsulClientFactory(); + _serviceBuilder = new DefaultConsulServiceBuilder(() => _config, _clientFactory, _factory.Object); + _provider = new ConsulProvider(_config, _factory.Object, _clientFactory, _serviceBuilder); + } + + [Fact] + public async Task Should_return_service_from_consul() + { + Arrange(); + var service1 = GivenService(50881); + _consulServiceEntries.Add(service1.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(1); + } + + [Fact] + public async Task Should_use_token() + { + Arrange(); + const string token = "test token"; + var service1 = GivenService(50881); + _consulServiceEntries.Add(service1.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, nameof(Should_use_token), token); + _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(1); + _receivedToken.ShouldBe(token); + } + + [Fact] + public async Task Should_not_return_services_with_invalid_address() + { + Arrange(); + var service1 = GivenService(50881, "http://localhost"); + var service2 = GivenService(50888, "http://localhost"); + _consulServiceEntries.Add(service1.ToServiceEntry()); + _consulServiceEntries.Add(service2.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); + } + + [Fact] + public async Task Should_not_return_services_with_empty_address() + { + Arrange(); + var service1 = GivenService(50881).WithAddress(string.Empty); + var service2 = GivenService(50888).WithAddress(null); + _consulServiceEntries.Add(service1.ToServiceEntry()); + _consulServiceEntries.Add(service2.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); + } + + [Fact] + public async Task Should_not_return_services_with_invalid_port() + { + Arrange(); + var service1 = GivenService(-1); + var service2 = GivenService(0); + _consulServiceEntries.Add(service1.ToServiceEntry()); + _consulServiceEntries.Add(service2.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); + } + + [Fact] + public async Task GetAsync_NoEntries_ShouldLogWarning() + { + Arrange(); + _consulServiceEntries.Clear(); // NoEntries + _logger.Setup(x => x.LogWarning(It.IsAny>())).Verifiable(); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().ShouldBeEmpty(); + var expected = $"Consul Provider: No service entries found for '{nameof(GetAsync_NoEntries_ShouldLogWarning)}' service!"; + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); + } + + private static AgentService GivenService(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() + { + Service = serviceName, + Address = address ?? "localhost", + Port = port, + ID = id ?? Guid.NewGuid().ToString(), + Tags = tags ?? Array.Empty(), + }; + + private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning() + { + foreach (var entry in _consulServiceEntries) + { + var service = entry.Service; + var expected = $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."; + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider([CallerMemberName] string serviceName = "test") + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(_fakeConsulServiceDiscoveryUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_fakeConsulServiceDiscoveryUrl) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) + { + _receivedToken = values.First(); + } + + var json = JsonConvert.SerializeObject(_consulServiceEntries); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + }) + .Build(); + _fakeConsulBuilder.Start(); + } +} diff --git a/test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs b/test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs new file mode 100644 index 000000000..25dc8d950 --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs @@ -0,0 +1,200 @@ +using Castle.Components.DictionaryAdapter.Xml; +using Consul; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Xml.Linq; + +namespace Ocelot.UnitTests.Consul; + +public sealed class DefaultConsulServiceBuilderTests +{ + private DefaultConsulServiceBuilder sut; + private readonly Func configurationFactory; + private readonly Mock clientFactory; + private readonly Mock loggerFactory; + private readonly Mock logger; + private ConsulRegistryConfiguration _configuration; + + private ConsulRegistryConfiguration GetConfiguration() => _configuration; + + public DefaultConsulServiceBuilderTests() + { + configurationFactory = GetConfiguration; + clientFactory = new(); + clientFactory.Setup(x => x.Get(It.IsAny())) + .Returns(new ConsulClient()); + logger = new(); + loggerFactory = new(); + loggerFactory.Setup(x => x.CreateLogger()) + .Returns(logger.Object); + } + + private void Arrange([CallerMemberName] string testName = null) + { + _configuration = new(null, null, 0, testName, null); + sut = new DefaultConsulServiceBuilder(configurationFactory, clientFactory.Object, loggerFactory.Object); + } + + [Fact] + public void Ctor_PrivateMembers_PropertiesAreInitialized() + { + Arrange(); + var methodClient = sut.GetType().GetProperty("Client", BindingFlags.NonPublic | BindingFlags.Instance); + var methodLogger = sut.GetType().GetProperty("Logger", BindingFlags.NonPublic | BindingFlags.Instance); + + // Act + var actualConfiguration = sut.Configuration; + var actualClient = methodClient.GetValue(sut); + var actualLogger = methodLogger.GetValue(sut); + + // Assert + actualConfiguration.ShouldNotBeNull().ShouldBe(_configuration); + actualClient.ShouldNotBeNull(); + actualLogger.ShouldNotBeNull(); + } + + private static Type Me { get; } = typeof(DefaultConsulServiceBuilder); + private static MethodInfo GetNode { get; } = Me.GetMethod("GetNode", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetNode_EntryBranch_ReturnsEntryNode() + { + Arrange(); + Node node = new() { Name = nameof(GetNode_EntryBranch_ReturnsEntryNode) }; + ServiceEntry entry = new() { Node = node }; + + // Act + var actual = GetNode.Invoke(sut, new object[] { entry, null }) as Node; + + // Assert + actual.ShouldNotBeNull().ShouldBe(node); + actual.Name.ShouldBe(node.Name); + } + + [Fact] + public void GetNode_NodesBranch_ReturnsNodeFromCollection() + { + Arrange(); + ServiceEntry entry = new() + { + Node = null, + Service = new() { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) }, + }; + Node[] nodes = null; + + // Act, Assert: nodes is null + var actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + + // Arrange, Act, Assert: nodes has items, happy path + var node = new Node { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) }; + nodes = new[] { node }; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldNotBeNull().ShouldBe(node); + actual.Address.ShouldBe(entry.Service.Address); + + // Arrange, Act, Assert: nodes has items, some nulls in entry + entry.Service.Address = null; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + + entry.Service = null; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + + entry = null; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + } + + private static MethodInfo GetDownstreamHost { get; } = Me.GetMethod("GetDownstreamHost", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetDownstreamHost_BothBranches_NameOrAddress() + { + Arrange(); + + // Arrange, Act, Assert: node branch + ServiceEntry entry = new() + { + Service = new() { Address = nameof(GetDownstreamHost_BothBranches_NameOrAddress) }, + }; + var node = new Node { Name = "test1" }; + var actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldNotBeNull().ShouldBe("test1"); + + // Arrange, Act, Assert: entry branch + node = null; + actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldNotBeNull().ShouldBe(nameof(GetDownstreamHost_BothBranches_NameOrAddress)); + } + + private static MethodInfo GetServiceVersion { get; } = Me.GetMethod("GetServiceVersion", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetServiceVersion_TagsIsNull_EmptyString() + { + Arrange(); + + // Arrange, Act, Assert: collection is null + ServiceEntry entry = new() + { + Service = new() { Tags = null }, + }; + Node node = null; + var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldBe(string.Empty); + + // Arrange, Act, Assert: collection has no version tag + entry.Service.Tags = new[] { "test" }; + actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldBe(string.Empty); + } + + [Fact] + public void GetServiceVersion_HasTags_HappyPath() + { + Arrange(); + + // Arrange + var tags = new string[] { "test", "version-v2" }; + ServiceEntry entry = new() + { + Service = new() { Tags = tags }, + }; + Node node = null; + + // Act + var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; + + // Assert + actual.ShouldBe("v2"); + } + + private static MethodInfo GetServiceTags { get; } = Me.GetMethod("GetServiceTags", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetServiceTags_BothBranches() + { + Arrange(); + + // Arrange, Act, Assert: collection is null + ServiceEntry entry = new() + { + Service = new() { Tags = null }, + }; + Node node = null; + var actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable; + actual.ShouldNotBeNull().ShouldBeEmpty(); + + // Arrange, Act, Assert: happy path + entry.Service.Tags = new string[] { "1", "2", "3" }; + actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable; + actual.ShouldNotBeNull().ShouldNotBeEmpty(); + actual.Count().ShouldBe(3); + actual.ShouldContain("3"); + } +} diff --git a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs index f324f4b4d..c1a2ed096 100644 --- a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs @@ -1,73 +1,105 @@ -using Microsoft.AspNetCore.Hosting; +using Consul; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using Ocelot.Values; using System.Reflection; -namespace Ocelot.UnitTests.Consul +namespace Ocelot.UnitTests.Consul; + +public class OcelotBuilderExtensionsTests : UnitTest { - public class OcelotBuilderExtensionsTests + private readonly IServiceCollection _services; + private readonly IConfiguration _configRoot; + + public OcelotBuilderExtensionsTests() + { + _configRoot = new ConfigurationRoot(new List()); + _services = new ServiceCollection(); + _services.AddSingleton(GetHostingEnvironment()); + _services.AddSingleton(_configRoot); + } + + private static IWebHostEnvironment GetHostingEnvironment() { - private readonly IServiceCollection _services; - private readonly IConfiguration _configRoot; - private IOcelotBuilder _ocelotBuilder; - private Exception _ex; + var environment = new Mock(); + environment.Setup(e => e.ApplicationName) + .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); + return environment.Object; + } - public OcelotBuilderExtensionsTests() + [Fact] + public void AddConsul_ShouldSetUpConsul() + { + // Arrange + Exception ex = null; + try { - _configRoot = new ConfigurationRoot(new List()); - _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); - _services.AddSingleton(_configRoot); + // Act + var builder = _services.AddOcelot(_configRoot); + builder.AddConsul(); } - - private static IWebHostEnvironment GetHostingEnvironment() + catch (Exception e) { - var environment = new Mock(); - environment - .Setup(e => e.ApplicationName) - .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); - - return environment.Object; + ex = e; } - [Fact] - public void should_set_up_consul() + // Assert + ex.ShouldBeNull(); + } + + [Fact] + public void AddConfigStoredInConsul_ShouldSetUpConsul() + { + // Arrange + Exception ex = null; + try { - this.Given(x => WhenISetUpOcelotServices()) - .When(x => WhenISetUpConsul()) - .Then(x => ThenAnExceptionIsntThrown()) - .BDDfy(); + // Act + var builder = _services.AddOcelot(_configRoot); + builder.AddConsul().AddConfigStoredInConsul(); } - - private void WhenISetUpOcelotServices() + catch (Exception e) { - try - { - _ocelotBuilder = _services.AddOcelot(_configRoot); - } - catch (Exception e) - { - _ex = e; - } + ex = e; } - private void WhenISetUpConsul() + // Assert + ex.ShouldBeNull(); + } + + [Fact] + public void AddConsulGeneric_TServiceBuilder_ShouldSetUpConsul() + { + // Arrange + Exception ex = null; + IOcelotBuilder builder = null; + try { - try - { - _ocelotBuilder.AddConsul().AddConfigStoredInConsul(); - } - catch (Exception e) - { - _ex = e; - } + // Act + builder = _services + .AddOcelot(_configRoot) + .AddConsul(); } - - private void ThenAnExceptionIsntThrown() + catch (Exception e) { - _ex.ShouldBeNull(); + ex = e; } + + // Assert + ex.ShouldBeNull(); + builder.ShouldNotBeNull(); + builder.Services.SingleOrDefault(s => s.ServiceType == typeof(IConsulServiceBuilder)).ShouldNotBeNull(); } } + +internal class FakeConsulServiceBuilder : IConsulServiceBuilder +{ + public ConsulRegistryConfiguration Configuration => throw new NotImplementedException(); + public IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) => throw new NotImplementedException(); + public Service CreateService(ServiceEntry serviceEntry, Node serviceNode) => throw new NotImplementedException(); + public bool IsValid(ServiceEntry entry) => throw new NotImplementedException(); +} diff --git a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs index a5dc99585..5b8eda527 100644 --- a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Consul { - public class PollingConsulServiceDiscoveryProviderTests + public class PollingConsulServiceDiscoveryProviderTests : UnitTest { private readonly int _delay; private readonly List _services; diff --git a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs index f71b4ffe5..d7b676a23 100644 --- a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs @@ -3,6 +3,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.UnitTests.Consul; diff --git a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs index 1b143273f..aa21ff625 100644 --- a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Controllers { - public class FileConfigurationControllerTests + public class FileConfigurationControllerTests : UnitTest { private readonly FileConfigurationController _controller; private readonly Mock _repo; diff --git a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs index 067d06c68..71c2b4fa6 100644 --- a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Controllers { - public class OutputCacheControllerTests + public class OutputCacheControllerTests : UnitTest { private readonly OutputCacheController _controller; private readonly Mock> _cache; diff --git a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs index 3514ca97d..f0e322fa8 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs @@ -199,14 +199,14 @@ private void GivenMultipleConfigurationFiles(string folder, bool withEnvironment RequestIdKey = "RequestIdKey", }; - private static List GetFileAggregatesRouteData() => - [ + private static List GetFileAggregatesRouteData() => new() + { new() { - RouteKeys = [ "KeyB", "KeyBB" ], + RouteKeys = new() { "KeyB", "KeyBB" }, UpstreamPathTemplate = "UpstreamPathTemplate", }, - ]; + }; private static FileRoute GetRoute(string suffix) => new() { @@ -214,16 +214,16 @@ private static List GetFileAggregatesRouteData() => DownstreamPathTemplate = "DownstreamPathTemplate" + suffix, Key = "Key" + suffix, UpstreamHost = "UpstreamHost" + suffix, - UpstreamHttpMethod = ["UpstreamHttpMethod" + suffix], - DownstreamHostAndPorts = - [ + UpstreamHttpMethod = new() { "UpstreamHttpMethod" + suffix }, + DownstreamHostAndPorts = new() + { new("Host"+suffix, 80), - ], + }, }; - private static List GetServiceARoutes() => [GetRoute("A")]; - private static List GetServiceBRoutes() => [GetRoute("B"), GetRoute("BB")]; - private static List GetEnvironmentSpecificRoutes() => [GetRoute("Spec")]; + private static List GetServiceARoutes() => new() { GetRoute("A") }; + private static List GetServiceBRoutes() => new() { GetRoute("B"), GetRoute("BB") }; + private static List GetEnvironmentSpecificRoutes() => new() { GetRoute("Spec") }; private void GivenTheEnvironmentIs(string folder, [CallerMemberName] string testName = null) { diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index 3879d4c0a..262014927 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -27,7 +27,7 @@ namespace Ocelot.UnitTests.DependencyInjection { - public class OcelotBuilderTests + public class OcelotBuilderTests : UnitTest { private readonly IConfiguration _configRoot; private readonly IServiceCollection _services; diff --git a/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 2ff94d48f..87a7c97d9 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -41,7 +41,7 @@ public void FindConfiguration_HasDescriptor_HappyPath(bool hasConfig) // Act var method = typeof(Extensions).GetMethod("FindConfiguration", BindingFlags.NonPublic | BindingFlags.Static); - var actual = (IConfiguration)method.Invoke(null, [services, env]); + var actual = (IConfiguration)method.Invoke(null, new object[] { services, env }); // Assert actual.ShouldNotBeNull(); diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs index 3281aaa1a..efe212166 100644 --- a/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.DownstreamPathManipulation { - public class ChangeDownstreamPathTemplateTests + public class ChangeDownstreamPathTemplateTests : UnitTest { private readonly ChangeDownstreamPathTemplate _changeDownstreamPath; private DownstreamPathTemplate _downstreamPathTemplate; diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs index 9be0a8f02..4a1fcd7f3 100644 --- a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.DownstreamPathManipulation { - public class ClaimsToDownstreamPathMiddlewareTests + public class ClaimsToDownstreamPathMiddlewareTests : UnitTest { private readonly Mock _changePath; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs index 0ead908aa..741af9d54 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { - public class DownstreamRouteCreatorTests + public class DownstreamRouteCreatorTests : UnitTest { private readonly DownstreamRouteCreator _creator; private readonly QoSOptions _qoSOptions; @@ -16,7 +16,8 @@ public class DownstreamRouteCreatorTests private Response _result; private string _upstreamHost; private string _upstreamUrlPath; - private string _upstreamHttpMethod; + private string _upstreamHttpMethod; + private Dictionary _upstreamHeaders; private IInternalConfiguration _configuration; private readonly Mock _qosOptionsCreator; private Response _resultTwo; @@ -38,7 +39,17 @@ public DownstreamRouteCreatorTests() [Fact] public void should_create_downstream_route() { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .When(_ => WhenICreate()) @@ -65,7 +76,17 @@ public void should_create_downstream_route_with_rate_limit_options() var routes = new List { route }; - var configuration = new InternalConfiguration(routes, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + routes, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .When(_ => WhenICreate()) @@ -77,7 +98,17 @@ public void should_create_downstream_route_with_rate_limit_options() [Fact] public void should_cache_downstream_route() { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, "/geoffisthebest/")) .When(_ => WhenICreate()) @@ -90,7 +121,17 @@ public void should_cache_downstream_route() [Fact] public void should_not_cache_downstream_route() { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, "/geoffistheworst/")) .When(_ => WhenICreate()) @@ -104,7 +145,17 @@ public void should_not_cache_downstream_route() public void should_create_downstream_route_with_no_path() { var upstreamUrlPath = "/auth/"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) .When(_ => WhenICreate()) @@ -116,7 +167,17 @@ public void should_create_downstream_route_with_no_path() public void should_create_downstream_route_with_only_first_segment_no_traling_slash() { var upstreamUrlPath = "/auth"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) .When(_ => WhenICreate()) @@ -128,7 +189,17 @@ public void should_create_downstream_route_with_only_first_segment_no_traling_sl public void should_create_downstream_route_with_segments_no_traling_slash() { var upstreamUrlPath = "/auth/test"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrHigher); this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) .When(_ => WhenICreate()) @@ -140,7 +211,17 @@ public void should_create_downstream_route_with_segments_no_traling_slash() public void should_create_downstream_route_and_remove_query_string() { 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")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) .When(_ => WhenICreate()) @@ -152,7 +233,17 @@ public void should_create_downstream_route_and_remove_query_string() public void should_create_downstream_route_for_sticky_sessions() { 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")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .When(_ => WhenICreate()) @@ -168,7 +259,17 @@ public void should_create_downstream_route_with_qos() .WithTimeoutValue(1) .Build(); - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .And(_ => GivenTheQosCreatorReturns(qoSOptions)) @@ -180,7 +281,17 @@ public void should_create_downstream_route_with_qos() [Fact] public void should_create_downstream_route_with_handler_options() { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .When(_ => WhenICreate()) @@ -259,7 +370,8 @@ private void GivenTheConfiguration(IInternalConfiguration config) { _upstreamHost = "doesnt matter"; _upstreamUrlPath = "/auth/test"; - _upstreamHttpMethod = "GET"; + _upstreamHttpMethod = "GET"; + _upstreamHeaders = new Dictionary(); _configuration = config; } @@ -278,12 +390,12 @@ private void ThenTheHandlerOptionsAreSet() private void WhenICreate() { - _result = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); + _result = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); } private void WhenICreateAgain() { - _resultTwo = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); + _resultTwo = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); } private void ThenTheDownstreamRoutesAreTheSameReference() diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index f743f6914..c96b81e69 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { - public class DownstreamRouteFinderMiddlewareTests + public class DownstreamRouteFinderMiddlewareTests : UnitTest { private readonly Mock _finder; private readonly Mock _factory; @@ -39,7 +39,17 @@ public DownstreamRouteFinderMiddlewareTests() [Fact] public void should_call_scoped_data_repository_correctly() { - var config = new InternalConfiguration(null, null, new ServiceProviderConfigurationBuilder().Build(), string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + var config = new InternalConfiguration( + null, + null, + new ServiceProviderConfigurationBuilder().Build(), + string.Empty, + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") @@ -74,7 +84,7 @@ private void GivenTheDownStreamRouteFinderReturns(DownstreamRouteHolder downstre { _downstreamRoute = new OkResponse(downstreamRoute); _finder - .Setup(x => x.Get(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Get(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) .Returns(_downstreamRoute); } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index 46654d8f4..2c2e04510 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -2,31 +2,37 @@ using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.UnitTests.DownstreamRouteFinder -{ - public class DownstreamRouteFinderTests +{ + public class DownstreamRouteFinderTests : UnitTest { private readonly IDownstreamRouteProvider _downstreamRouteFinder; - private readonly Mock _mockMatcher; - private readonly Mock _finder; + private readonly Mock _mockUrlMatcher; + private readonly Mock _mockHeadersMatcher; + private readonly Mock _urlPlaceholderFinder; + private readonly Mock _headerPlaceholderFinder; private string _upstreamUrlPath; private Response _result; private List _routesConfig; private InternalConfiguration _config; private Response _match; private string _upstreamHttpMethod; - private string _upstreamHost; + private string _upstreamHost; + private Dictionary _upstreamHeaders; private string _upstreamQuery; public DownstreamRouteFinderTests() { - _mockMatcher = new Mock(); - _finder = new Mock(); - _downstreamRouteFinder = new Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder(_mockMatcher.Object, _finder.Object); + _mockUrlMatcher = new Mock(); + _mockHeadersMatcher = new Mock(); + _urlPlaceholderFinder = new Mock(); + _headerPlaceholderFinder = new Mock(); + _downstreamRouteFinder = new Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder(_mockUrlMatcher.Object, _urlPlaceholderFinder.Object, _mockHeadersMatcher.Object, _headerPlaceholderFinder.Object); } [Fact] @@ -37,6 +43,7 @@ public void should_return_highest_priority_when_first() this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -58,7 +65,8 @@ public void should_return_highest_priority_when_first() .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) .Build(), }, string.Empty, serviceProviderConfig)) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), @@ -83,6 +91,7 @@ public void should_return_highest_priority_when_lowest() this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -104,7 +113,8 @@ public void should_return_highest_priority_when_lowest() .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) .Build(), }, string.Empty, serviceProviderConfig)) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), @@ -130,6 +140,7 @@ public void should_return_route() .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -143,7 +154,8 @@ public void should_return_route() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -172,6 +184,8 @@ public void should_not_append_slash_to_upstream_url_path() .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -185,7 +199,8 @@ public void should_not_append_slash_to_upstream_url_path() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -215,6 +230,7 @@ public void should_return_route_if_upstream_path_and_upstream_template_are_the_s x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -228,7 +244,8 @@ public void should_return_route_if_upstream_path_and_upstream_template_are_the_s .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -256,6 +273,7 @@ public void should_return_correct_route_for_http_verb() x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -278,7 +296,8 @@ public void should_return_correct_route_for_http_verb() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then( @@ -315,7 +334,8 @@ public void should_not_return_route() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -334,6 +354,7 @@ public void should_return_correct_route_for_http_verb_setting_multiple_upstream_ x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -347,7 +368,8 @@ public void should_return_correct_route_for_http_verb_setting_multiple_upstream_ .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then( @@ -375,6 +397,7 @@ public void should_return_correct_route_for_http_verb_setting_all_upstream_http_ x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -388,7 +411,8 @@ public void should_return_correct_route_for_http_verb_setting_all_upstream_http_ .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then( @@ -416,6 +440,7 @@ public void should_not_return_route_for_http_verb_not_setting_in_upstream_http_m x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -429,7 +454,8 @@ public void should_not_return_route_for_http_verb_not_setting_in_upstream_http_m .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenAnErrorResponseIsReturned()) @@ -447,6 +473,7 @@ public void should_return_route_when_host_matches() .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -461,7 +488,8 @@ public void should_return_route_when_host_matches() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -491,6 +519,7 @@ public void should_return_route_when_upstreamhost_is_null() .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -504,7 +533,8 @@ public void should_return_route_when_upstreamhost_is_null() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -532,6 +562,7 @@ public void should_not_return_route_when_host_doesnt_match() this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) .And(x => GivenTheUpstreamHostIs("DONTMATCH")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -556,7 +587,8 @@ public void should_not_return_route_when_host_doesnt_match() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenAnErrorResponseIsReturned()) @@ -572,6 +604,7 @@ public void should_not_return_route_when_host_doesnt_match_with_empty_upstream_h this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) .And(x => GivenTheUpstreamHostIs("DONTMATCH")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -586,7 +619,8 @@ public void should_not_return_route_when_host_doesnt_match_with_empty_upstream_h .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenAnErrorResponseIsReturned()) @@ -602,6 +636,7 @@ public void should_return_route_when_host_does_match_with_empty_upstream_http_me this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) .And(x => GivenTheUpstreamHostIs("MATCH")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -616,7 +651,8 @@ public void should_return_route_when_host_does_match_with_empty_upstream_http_me .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 0)) @@ -633,6 +669,7 @@ public void should_return_route_when_host_matches_but_null_host_on_same_path_fir .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -656,7 +693,8 @@ public void should_return_route_when_host_matches_but_null_host_on_same_path_fir .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -675,8 +713,124 @@ public void should_return_route_when_host_matches_but_null_host_on_same_path_fir .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 0)) .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 1)) .BDDfy(); - } - + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Should_return_route_when_upstream_headers_match() + { + // Arrange + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + var upstreamHeaders = new Dictionary() + { + ["header1"] = "headerValue1", + ["header2"] = "headerValue2", + ["header3"] = "headerValue3", + }; + var upstreamHeadersConfig = new Dictionary() + { + ["header1"] = new UpstreamHeaderTemplate("headerValue1", "headerValue1"), + ["header2"] = new UpstreamHeaderTemplate("headerValue2", "headerValue2"), + }; + var urlPlaceholders = new List { new PlaceholderNameAndValue("url", "urlValue") }; + var headerPlaceholders = new List { new PlaceholderNameAndValue("header", "headerValue") }; + + GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/"); + GivenTheUpstreamHeadersIs(upstreamHeaders); + GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(urlPlaceholders)); + GivenTheHeaderPlaceholderAndNameFinderReturns(headerPlaceholders); + GivenTheConfigurationIs( + 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(), + }, + string.Empty, + serviceProviderConfig); + GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true))); + GivenTheHeadersMatcherReturns(true); + GivenTheUpstreamHttpMethodIs("Get"); + + // Act + WhenICallTheFinder(); + + // Assert + ThenTheFollowingIsReturned(new DownstreamRouteHolder( + 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() + )); + ThenTheUrlMatcherIsCalledCorrectly(); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Should_not_return_route_when_upstream_headers_dont_match() + { + // Arrange + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + var upstreamHeadersConfig = new Dictionary() + { + ["header1"] = new UpstreamHeaderTemplate("headerValue1", "headerValue1"), + ["header2"] = new UpstreamHeaderTemplate("headerValue2", "headerValue2"), + }; + + GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/"); + GivenTheUpstreamHeadersIs(new Dictionary() { { "header1", "headerValue1" } }); + GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List())); + GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); + GivenTheConfigurationIs(new List + { + 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(), + }, string.Empty, serviceProviderConfig + ); + GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true))); + GivenTheHeadersMatcherReturns(false); + GivenTheUpstreamHttpMethodIs("Get"); + + // Act + WhenICallTheFinder(); + + // Assert + ThenAnErrorResponseIsReturned(); + } + private void GivenTheUpstreamHostIs(string upstreamHost) { _upstreamHost = upstreamHost; @@ -684,14 +838,26 @@ private void GivenTheUpstreamHostIs(string upstreamHost) private void GivenTheTemplateVariableAndNameFinderReturns(Response> response) { - _finder + _urlPlaceholderFinder .Setup(x => x.Find(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response); } + private void GivenTheHeaderPlaceholderAndNameFinderReturns(List placeholders) + { + _headerPlaceholderFinder + .Setup(x => x.Find(It.IsAny>(), It.IsAny>())) + .Returns(placeholders); + } + private void GivenTheUpstreamHttpMethodIs(string upstreamHttpMethod) { _upstreamHttpMethod = upstreamHttpMethod; + } + + private void GivenTheUpstreamHeadersIs(Dictionary upstreamHeaders) + { + _upstreamHeaders = upstreamHeaders; } private void ThenAnErrorResponseIsReturned() @@ -701,40 +867,57 @@ private void ThenAnErrorResponseIsReturned() private void ThenTheUrlMatcherIsCalledCorrectly() { - _mockMatcher + _mockUrlMatcher .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once); } private void ThenTheUrlMatcherIsCalledCorrectly(int times, int index = 0) { - _mockMatcher + _mockUrlMatcher .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[index].UpstreamTemplatePattern), Times.Exactly(times)); } private void ThenTheUrlMatcherIsCalledCorrectly(string expectedUpstreamUrlPath) { - _mockMatcher + _mockUrlMatcher .Verify(x => x.Match(expectedUpstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once); } private void ThenTheUrlMatcherIsNotCalled() { - _mockMatcher + _mockUrlMatcher .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Never); } private void GivenTheUrlMatcherReturns(Response match) { _match = match; - _mockMatcher + _mockUrlMatcher .Setup(x => x.Match(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_match); + } + + private void GivenTheHeadersMatcherReturns(bool headersMatch) + { + _mockHeadersMatcher + .Setup(x => x.Match(It.IsAny>(), It.IsAny>())) + .Returns(headersMatch); } private void GivenTheConfigurationIs(List routesConfig, string adminPath, ServiceProviderConfiguration serviceProviderConfig) { _routesConfig = routesConfig; - _config = new InternalConfiguration(_routesConfig, adminPath, serviceProviderConfig, string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + _config = new InternalConfiguration( + _routesConfig, + adminPath, + serviceProviderConfig, + string.Empty, + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); } private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) @@ -745,7 +928,7 @@ private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) private void WhenICallTheFinder() { - _result = _downstreamRouteFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost); + _result = _downstreamRouteFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); } private void ThenTheFollowingIsReturned(DownstreamRouteHolder expected) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs index 209d67b72..2eb2a092b 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs @@ -2,6 +2,7 @@ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; +using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; @@ -9,7 +10,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { using Ocelot.DownstreamRouteFinder.Finder; - public class DownstreamRouteProviderFactoryTests + public class DownstreamRouteProviderFactoryTests : UnitTest { private readonly DownstreamRouteProviderFactory _factory; private IInternalConfiguration _config; @@ -21,7 +22,9 @@ public DownstreamRouteProviderFactoryTests() { var services = new ServiceCollection(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -148,12 +151,32 @@ private void WhenIGet() private void GivenTheRoutes(List routes) { - _config = new InternalConfiguration(routes, string.Empty, null, string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + _config = new InternalConfiguration( + routes, + string.Empty, + null, + string.Empty, + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); } private void GivenTheRoutes(List routes, ServiceProviderConfiguration config) { - _config = new InternalConfiguration(routes, string.Empty, config, string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + _config = new InternalConfiguration( + routes, + string.Empty, + config, + string.Empty, + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); } } } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinderTests.cs new file mode 100644 index 000000000..b1a767602 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinderTests.cs @@ -0,0 +1,220 @@ +using Ocelot.DownstreamRouteFinder.HeaderMatcher; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Values; + +namespace Ocelot.UnitTests.DownstreamRouteFinder.HeaderMatcher; + +[Trait("PR", "1312")] +[Trait("Feat", "360")] +public class HeaderPlaceholderNameAndValueFinderTests : UnitTest +{ + private readonly IHeaderPlaceholderNameAndValueFinder _finder; + private Dictionary _upstreamHeaders; + private Dictionary _upstreamHeaderTemplates; + private List _result; + + public HeaderPlaceholderNameAndValueFinderTests() + { + _finder = new HeaderPlaceholderNameAndValueFinder(); + } + + [Fact] + public void Should_return_no_placeholders() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary(); + var upstreamHeaders = new Dictionary(); + var expected = new List(); + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_one_placeholder_with_value_when_no_other_text() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^(?i)(?.+)$", "{header:countrycode}"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "PL", + }; + var expected = new List + { + new("{countrycode}", "PL"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_one_placeholder_with_value_when_other_text_on_the_right() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^(?.+)-V1$", "{header:countrycode}-V1"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "PL-V1", + }; + var expected = new List + { + new("{countrycode}", "PL"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_one_placeholder_with_value_when_other_text_on_the_left() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^V1-(?.+)$", "V1-{header:countrycode}"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "V1-PL", + }; + var expected = new List + { + new("{countrycode}", "PL"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_one_placeholder_with_value_when_other_texts_surrounding() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^cc:(?.+)-V1$", "cc:{header:countrycode}-V1"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "cc:PL-V1", + }; + var expected = new List + { + new("{countrycode}", "PL"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_two_placeholders_with_text_between() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["countryAndVersion"] = new("^(?i)(?.+)-(?.+)$", "{header:countrycode}-{header:version}"), + }; + var upstreamHeaders = new Dictionary + { + ["countryAndVersion"] = "PL-v1", + }; + var expected = new List + { + new("{countrycode}", "PL"), + new("{version}", "v1"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_placeholders_from_different_headers() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^(?i)(?.+)$", "{header:countrycode}"), + ["version"] = new("^(?i)(?.+)$", "{header:version}"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "PL", + ["version"] = "v1", + }; + var expected = new List + { + new("{countrycode}", "PL"), + new("{version}", "v1"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + private void GivenUpstreamHeaderTemplatesAre(Dictionary upstreaHeaderTemplates) + { + _upstreamHeaderTemplates = upstreaHeaderTemplates; + } + + private void GivenUpstreamHeadersAre(Dictionary upstreamHeaders) + { + _upstreamHeaders = upstreamHeaders; + } + + private void WhenICallFindPlaceholders() + { + var result = _finder.Find(_upstreamHeaders, _upstreamHeaderTemplates); + _result = new(result); + } + + private void TheResultIs(List expected) + { + _result.ShouldNotBeNull(); + _result.Count.ShouldBe(expected.Count); + _result.ForEach(x => expected.Any(e => e.Name == x.Name && e.Value == x.Value).ShouldBeTrue()); + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcherTests.cs new file mode 100644 index 000000000..fae3cc0c5 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcherTests.cs @@ -0,0 +1,293 @@ +using Ocelot.DownstreamRouteFinder.HeaderMatcher; +using Ocelot.Values; + +namespace Ocelot.UnitTests.DownstreamRouteFinder.HeaderMatcher; + +[Trait("PR", "1312")] +[Trait("Feat", "360")] +public class HeadersToHeaderTemplatesMatcherTests : UnitTest +{ + private readonly IHeadersToHeaderTemplatesMatcher _headerMatcher; + private Dictionary _upstreamHeaders; + private Dictionary _templateHeaders; + private bool _result; + + public HeadersToHeaderTemplatesMatcherTests() + { + _headerMatcher = new HeadersToHeaderTemplatesMatcher(); + } + + [Fact] + public void Should_match_when_no_template_headers() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValue", + }; + var templateHeaders = new Dictionary(); + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_match_the_same_headers() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValue", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_not_match_the_same_headers_when_differ_case_and_case_sensitive() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "ANYHEADERVALUE", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + [Fact] + public void Should_match_the_same_headers_when_differ_case_and_case_insensitive() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "ANYHEADERVALUE", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_not_match_different_headers_values() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValueDifferent", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + [Fact] + public void Should_not_match_the_same_headers_names() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeaderDifferent"] = "anyHeaderValue", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + [Fact] + public void Should_match_all_the_same_headers() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValue", + ["notNeededHeader"] = "notNeededHeaderValue", + ["secondHeader"] = "secondHeaderValue", + ["thirdHeader"] = "thirdHeaderValue", + }; + var templateHeaders = new Dictionary() + { + ["secondHeader"] = new("^(?i)secondHeaderValue$", "secondHeaderValue"), + ["thirdHeader"] = new("^(?i)thirdHeaderValue$", "thirdHeaderValue"), + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_not_match_the_headers_when_one_of_them_different() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValue", + ["notNeededHeader"] = "notNeededHeaderValue", + ["secondHeader"] = "secondHeaderValueDIFFERENT", + ["thirdHeader"] = "thirdHeaderValue", + }; + var templateHeaders = new Dictionary() + { + ["secondHeader"] = new("^(?i)secondHeaderValue$", "secondHeaderValue"), + ["thirdHeader"] = new("^(?i)thirdHeaderValue$", "thirdHeaderValue"), + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + [Fact] + public void Should_match_the_header_with_placeholder() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "PL", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)(?.+)$", "{header:countrycode}"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_match_the_header_with_placeholders() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "PL-V1", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)(?.+)-(?.+)$", "{header:countrycode}-{header:version}"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_not_match_the_header_with_placeholders() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "PL", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)(?.+)-(?.+)$", "{header:countrycode}-{header:version}"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + private void GivenIHaveUpstreamHeaders(Dictionary upstreamHeaders) + { + _upstreamHeaders = upstreamHeaders; + } + + private void GivenIHaveTemplateHeadersInRoute(Dictionary templateHeaders) + { + _templateHeaders = templateHeaders; + } + + private void WhenIMatchTheHeaders() + { + _result = _headerMatcher.Match(_upstreamHeaders, _templateHeaders); + } + + private void ThenTheResultIsTrue() + { + _result.ShouldBeTrue(); + } + + private void ThenTheResultIsFalse() + { + _result.ShouldBeFalse(); + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index 6cddb2f53..4117fc562 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher { - public class RegExUrlMatcherTests + public class RegExUrlMatcherTests : UnitTest { private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; private string _path; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs index 606b63714..4dc610a50 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher { - public class UrlPathPlaceholderNameAndValueFinderTests + public class UrlPathPlaceholderNameAndValueFinderTests : UnitTest { private readonly IPlaceholderNameAndValueFinder _finder; private string _downstreamUrlPath; diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs index b7ff203dd..0f25294ee 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator { - public class DownstreamPathPlaceholderReplacerTests + public class DownstreamPathPlaceholderReplacerTests : UnitTest { private DownstreamRouteHolder _downstreamRoute; private Response _result; diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index c6529e33d..8a05f8b13 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -12,589 +12,655 @@ using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.UnitTests.DownstreamUrlCreator +namespace Ocelot.UnitTests.DownstreamUrlCreator; + +public sealed class DownstreamUrlCreatorMiddlewareTests : UnitTest { - public class DownstreamUrlCreatorMiddlewareTests + // TODO: Convert to integration tests to use real IDownstreamPathPlaceholderReplacer service (no mocking). There are a lot of failings + // private readonly IDownstreamPathPlaceholderReplacer _downstreamUrlTemplateVariableReplacer; + private readonly Mock _downstreamUrlTemplateVariableReplacer; + + private OkResponse _downstreamPath; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private DownstreamUrlCreatorMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly HttpRequestMessage _request; + private readonly HttpContext _httpContext; + private readonly Mock _repo; + + public DownstreamUrlCreatorMiddlewareTests() { - private readonly Mock _downstreamUrlTemplateVariableReplacer; - private OkResponse _downstreamPath; - private readonly Mock _loggerFactory; - private readonly Mock _logger; - private DownstreamUrlCreatorMiddleware _middleware; - private readonly RequestDelegate _next; - private readonly HttpRequestMessage _request; - private readonly HttpContext _httpContext; - private readonly Mock _repo; - - public DownstreamUrlCreatorMiddlewareTests() - { - _repo = new Mock(); - _httpContext = new DefaultHttpContext(); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _downstreamUrlTemplateVariableReplacer = new Mock(); - _request = new HttpRequestMessage(HttpMethod.Get, "https://my.url/abc/?q=123"); - _next = context => Task.CompletedTask; - } + _repo = new Mock(); + _httpContext = new DefaultHttpContext(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _downstreamUrlTemplateVariableReplacer = new Mock(); + _request = new HttpRequestMessage(HttpMethod.Get, "https://my.url/abc/?q=123"); + _next = context => Task.CompletedTask; + } - [Fact] - public void Should_replace_scheme_and_path() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("any old string") + [Fact] + public void Should_replace_scheme_and_path() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/api/products/1")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123")) - .And(x => ThenTheQueryStringIs("?q=123")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123"); + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("/api/products/1"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123"); + ThenTheQueryStringIs("?q=123"); + } - [Fact] - public void Should_replace_query_string() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + [Fact] + public void Should_replace_query_string() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{subscriptionId}", "1"), + new("{unitId}", "2"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{subscriptionId}", "1"), - new("{unitId}", "2"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates")) - .And(x => ThenTheQueryStringIs(string.Empty)) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2"); + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates"); + ThenTheQueryStringIs(string.Empty); + } - [Fact] - public void Should_replace_query_string_but_leave_non_placeholder_queries() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + [Fact] + public void Should_replace_query_string_but_leave_non_placeholder_queries() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{subscriptionId}", "1"), + new("{unitId}", "2"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{subscriptionId}", "1"), - new("{unitId}", "2"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2&productId=2")) // unitId is the first - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2")) - .And(x => ThenTheQueryStringIs("?productId=2")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2&productId=2"); // unitId is the first + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2"); + ThenTheQueryStringIs("?productId=2"); + } - [Fact] - public void Should_replace_query_string_but_leave_non_placeholder_queries_2() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + [Fact] + public void Should_replace_query_string_but_leave_non_placeholder_queries_2() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{subscriptionId}", "1"), + new("{unitId}", "2"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new PlaceholderNameAndValue("{subscriptionId}", "1"), - new PlaceholderNameAndValue("{unitId}", "2"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?productId=2&unitId=2")) // unitId is the second - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2")) - .And(x => ThenTheQueryStringIs("?productId=2")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?productId=2&unitId=2"); // unitId is the second + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2"); + ThenTheQueryStringIs("?productId=2"); + } - [Fact] - public void Should_replace_query_string_exact_match() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates/{unitIdIty}") + [Fact] + public void Should_replace_query_string_exact_match() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates/{unitIdIty}") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{subscriptionId}", "1"), + new("{unitId}", "2"), + new("{unitIdIty}", "3"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{subscriptionId}", "1"), - new("{unitId}", "2"), - new("{unitIdIty}", "3"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2?unitIdIty=3")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates/3")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates/3")) - .And(x => ThenTheQueryStringIs(string.Empty)) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2?unitIdIty=3"); + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("api/units/1/2/updates/3"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates/3"); + ThenTheQueryStringIs(string.Empty); + } - [Fact] - public void Should_not_create_service_fabric_url() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("any old string") + [Fact] + public void Should_not_create_service_fabric_url() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/api/products/1")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123"); + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("/api/products/1"); - [Fact] - public void Should_create_service_fabric_url() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamScheme("http") - .WithServiceName("Ocelot/OcelotApp") - .WithUseServiceDiscovery(true) - .Build(); - - var downstreamRouteHolder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1")) - .BDDfy(); - } + // Act + WhenICallTheMiddleware(); - [Fact] - public void Should_create_service_fabric_url_with_query_string_for_stateless_service() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamScheme("http") - .WithServiceName("Ocelot/OcelotApp") - .WithUseServiceDiscovery(true) - .Build(); - - var downstreamRouteHolder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?Tom=test&laura=1")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?Tom=test&laura=1")) - .BDDfy(); - } + // Assert + ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123"); + } - [Fact] - public void Should_create_service_fabric_url_with_query_string_for_stateful_service() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamScheme("http") - .WithServiceName("Ocelot/OcelotApp") - .WithUseServiceDiscovery(true) - .Build(); - - var downstreamRouteHolder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1")) - .BDDfy(); - } + [Fact] + public void Should_create_service_fabric_url() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamScheme("http") + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + var downstreamRouteHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder().WithDownstreamRoute(downstreamRoute).Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(downstreamRouteHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("http://localhost:19081"); + GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1"); + } - [Fact] - public void Should_create_service_fabric_url_with_version_from_upstream_path_template() - { - var downstreamRoute = new DownstreamRouteHolder( - new List(), - new RouteBuilder().WithDownstreamRoute( - new DownstreamRouteBuilder() - .WithDownstreamScheme("http") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("/products").Build()) - .WithUseServiceDiscovery(true) - .Build() - ).Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/products", "Service_1.0/Api")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Service_1.0/Api/products?PartitionKind=test&PartitionKey=1")) - .BDDfy(); - } + [Fact] + public void Should_create_service_fabric_url_with_query_string_for_stateless_service() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamScheme("http") + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + var downstreamRouteHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder().WithDownstreamRoute(downstreamRoute).Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(downstreamRouteHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("http://localhost:19081?Tom=test&laura=1"); + GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?Tom=test&laura=1"); + } - [Fact(DisplayName = "473: " + nameof(Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different))] - public void Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different() - { - var methods = new List { "Post", "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue("/uc/Authorized/{servak}/{action}").Build()) - .WithDownstreamPathTemplate("/Authorized/{action}?server={servak}") - .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{action}", "1"), - new("{servak}", "2"), - }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=123456789")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/Authorized/1?server=2")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:5000/Authorized/1?refreshToken=123456789&server=2")) - .And(x => ThenTheQueryStringIs("?refreshToken=123456789&server=2")) - .BDDfy(); - } + [Fact] + public void Should_create_service_fabric_url_with_query_string_for_stateful_service() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamScheme("http") + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + var downstreamRouteHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder().WithDownstreamRoute(downstreamRoute).Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(downstreamRouteHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1"); + GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1"); + } - [Fact] - public void Should_not_replace_by_empty_scheme() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamScheme(string.Empty) - .WithServiceName("Ocelot/OcelotApp") - .WithUseServiceDiscovery(true) - .Build(); - - var downstreamRouteHolder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("https://localhost:19081?PartitionKind=test&PartitionKey=1")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1")) - .BDDfy(); - } + [Fact] + public void Should_create_service_fabric_url_with_version_from_upstream_path_template() + { + // Arrange + var route = new DownstreamRouteBuilder() + .WithDownstreamScheme("http") + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("/products").Build()) + .WithUseServiceDiscovery(true) + .Build(); + var routeHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder().WithDownstreamRoute(route).Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(routeHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1"); + GivenTheUrlReplacerWillReturnSequence("/products", "Service_1.0/Api"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:19081/Service_1.0/Api/products?PartitionKind=test&PartitionKey=1"); + } - [Fact(DisplayName = "952: " + nameof(Should_map_query_parameters_with_different_names))] - public void Should_map_query_parameters_with_different_names() - { - var methods = new List { "Post", "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue("/users?userId={userId}").Build()) - .WithDownstreamPathTemplate("/persons?personId={userId}") + [Fact] + [Trait("Bug", "473")] + public void Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different() + { + // Arrange + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/uc/Authorized/{servak}/{action}").Build()) + .WithDownstreamPathTemplate("/Authorized/{action}?server={servak}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{action}", "1"), + new("{servak}", "2"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - var config = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{userId}", "webley"), - }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/persons?personId=webley")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley")) - .And(x => ThenTheQueryStringIs($"?personId=webley")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=123456789"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn("/Authorized/1?server=2"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:5000/Authorized/1?server=2&refreshToken=123456789"); + ThenTheQueryStringIs("?server=2&refreshToken=123456789"); + } - [Fact(DisplayName = "952: " + nameof(Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ))] - public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() - { - var methods = new List { "Post", "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue("/users?userId={uid}").Build()) - .WithDownstreamPathTemplate("/persons?personId={uid}") + [Fact] + public void Should_not_replace_by_empty_scheme() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamScheme(string.Empty) + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + var downstreamRouteHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(downstreamRouteHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("https://localhost:19081?PartitionKind=test&PartitionKey=1"); + GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1"); + } + + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names() + { + // Arrange + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/users?userId={userId}").Build()) + .WithDownstreamPathTemplate("/persons?personId={userId}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{userId}", "webley"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - var config = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{uid}", "webley"), - }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/persons?personId=webley")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley&userId=webley")) - .And(x => ThenTheQueryStringIs($"?personId=webley&userId=webley")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn("/persons?personId=webley"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley"); + ThenTheQueryStringIs($"?personId=webley"); + } - [Theory(DisplayName = "1174: " + nameof(Should_forward_query_parameters_without_duplicates))] - [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] - [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z")] - public void Should_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) - { - var methods = new List { "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue("/contracts?{everythingelse}").Build()) - .WithDownstreamPathTemplate("/api/contracts?{everythingelse}") + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() + { + // Arrange + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/users?userId={uid}").Build()) + .WithDownstreamPathTemplate("/persons?personId={uid}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{uid}", "webley"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - var config = new ServiceProviderConfigurationBuilder().Build(); - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{everythingelse}", everythingelse), - }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000//contracts?{everythingelse}")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn($"/api/contracts?{everythingelse}")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/api/contracts?{expectedOrdered}")) - .And(x => ThenTheQueryStringIs($"?{expectedOrdered}")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn("/persons?personId=webley"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley&userId=webley"); + ThenTheQueryStringIs($"?personId=webley&userId=webley"); + } - [Theory] - [Trait("Bug", "748")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123", "{url}", "123", "/api/v1/test/123", "")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123?query=1", "{url}", "123", "/api/v1/test/123?query=1", "?query=1")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/?query=1", "{url}", "", "/api/v1/test/?query=1", "?query=1")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1?query=1", "{url}", "", "/api/v1/test?query=1", "?query=1")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/", "{url}", "", "/api/v1/test/", "")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1", "{url}", "", "/api/v1/test", "")] - public void should_fix_issue_748(string upstreamTemplate, string downstreamTemplate, string requestURL, string placeholderName, string placeholderValue, string downstreamURI, string queryString) - { - var methods = new List { "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue(upstreamTemplate).Build()) - .WithDownstreamPathTemplate(downstreamTemplate) + [Theory] + [Trait("Bug", "1174")] + [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12")] + [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z")] + public void Should_forward_query_parameters_without_duplicates(string everythingelse) + { + // Arrange + var methods = new List { "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/contracts?{everythingelse}").Build()) + .WithDownstreamPathTemplate("/api/contracts?{everythingelse}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{everythingelse}", everythingelse), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new(placeholderName, placeholderValue), - new("{version}", "v1"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000" + requestURL)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn(downstreamURI)) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:5000" + downstreamURI)) - .And(x => ThenTheQueryStringIs(queryString)) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs($"http://localhost:5000//contracts?{everythingelse}"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn($"/api/contracts?{everythingelse}"); + + // Act + WhenICallTheMiddleware(); + + // Assert + var query = everythingelse; + ThenTheDownstreamRequestUriIs($"http://localhost:5000/api/contracts?{query}"); + ThenTheQueryStringIs($"?{query}"); + } - private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) - { - var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null); - _httpContext.Items.SetIInternalConfiguration(configuration); - } + [Theory] + [Trait("Bug", "748")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123", "{url}", "123", "/api/v1/test/123", "")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123?query=1", "{url}", "123", "/api/v1/test/123?query=1", "?query=1")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/?query=1", "{url}", "", "/api/v1/test/?query=1", "?query=1")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1?query=1", "{url}", "", "/api/v1/test?query=1", "?query=1")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/", "{url}", "", "/api/v1/test/", "")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1", "{url}", "", "/api/v1/test", "")] + public void Should_fix_issue_748(string upstreamTemplate, string downstreamTemplate, string requestURL, string placeholderName, string placeholderValue, string downstreamURI, string queryString) + { + // Arrange + var methods = new List { "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue(upstreamTemplate).Build()) + .WithDownstreamPathTemplate(downstreamTemplate) + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new(placeholderName, placeholderValue), + new("{version}", "v1"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000" + requestURL); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn(downstreamURI); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:5000" + downstreamURI); + ThenTheQueryStringIs(queryString); + } - private void WhenICallTheMiddleware() - { - _middleware = new DownstreamUrlCreatorMiddleware(_next, _loggerFactory.Object, _downstreamUrlTemplateVariableReplacer.Object); - _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); - } + [Fact] + [Trait("Bug", "2002")] + public void Should_map_when_query_parameters_has_same_names_with_placeholder() + { + // Arrange + const string username = "bbenameur"; + const string groupName = "Paris"; + const string roleid = "123456"; + const string everything = "something=9874565"; + var withGetMethod = new List { "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}") + .Build()) + .WithDownstreamPathTemplate("/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}") + .WithUpstreamHttpMethod(withGetMethod) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{username}", username), + new("{groupName}", groupName), + new("{roleid}", roleid), + new("{everything}", everything), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(withGetMethod) + .Build() + )); + GivenTheDownstreamRequestUriIs($"http://localhost:5000/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn($"/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs($"http://localhost:5000/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}"); + ThenTheQueryStringIs($"?roleId={roleid}&{everything}"); + } - private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute) - { - _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) + { + var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null, null); + _httpContext.Items.SetIInternalConfiguration(configuration); + } - _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); - } + private void WhenICallTheMiddleware() + { + _middleware = new DownstreamUrlCreatorMiddleware(_next, _loggerFactory.Object, _downstreamUrlTemplateVariableReplacer.Object); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } - private void GivenTheDownstreamRequestUriIs(string uri) - { - _request.RequestUri = new Uri(uri); - _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_request)); - } + private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute) + { + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + } - private void GivenTheUrlReplacerWillReturnSequence(params string[] paths) - { - var setup = _downstreamUrlTemplateVariableReplacer - .SetupSequence(x => x.Replace(It.IsAny(), It.IsAny>())); - foreach (var path in paths) - { - var response = new OkResponse(new DownstreamPath(path)); - setup.Returns(response); - } - } + private void GivenTheDownstreamRequestUriIs(string uri) + { + _request.RequestUri = new Uri(uri); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_request)); + } - private void GivenTheUrlReplacerWillReturn(string path) + private void GivenTheUrlReplacerWillReturnSequence(params string[] paths) + { + var setup = _downstreamUrlTemplateVariableReplacer + .SetupSequence(x => x.Replace(It.IsAny(), It.IsAny>())); + foreach (var path in paths) { - _downstreamPath = new OkResponse(new DownstreamPath(path)); - _downstreamUrlTemplateVariableReplacer - .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) - .Returns(_downstreamPath); + var response = new OkResponse(new DownstreamPath(path)); + setup.Returns(response); } + } - private void ThenTheDownstreamRequestUriIs(string expectedUri) - { - _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); - } + private void GivenTheUrlReplacerWillReturn(string path) + { + _downstreamPath = new OkResponse(new DownstreamPath(path)); + _downstreamUrlTemplateVariableReplacer + .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) + .Returns(_downstreamPath); + } - private void ThenTheQueryStringIs(string queryString) - { - _httpContext.Items.DownstreamRequest().Query.ShouldBe(queryString); - } + private void ThenTheDownstreamRequestUriIs(string expectedUri) + { + _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); + } + + private void ThenTheQueryStringIs(string queryString) + { + _httpContext.Items.DownstreamRequest().Query.ShouldBe(queryString); } } diff --git a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs index b59c0bb68..9bb883aa3 100644 --- a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Errors { - public class ExceptionHandlerMiddlewareTests + public class ExceptionHandlerMiddlewareTests : UnitTest { private bool _shouldThrowAnException; private readonly Mock _repo; @@ -42,7 +42,7 @@ public ExceptionHandlerMiddlewareTests() [Fact] public void NoDownstreamException() { - var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -55,7 +55,7 @@ public void NoDownstreamException() [Fact] public void DownstreamException() { - var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -67,7 +67,7 @@ public void DownstreamException() [Fact] public void ShouldSetRequestId() { - var config = new InternalConfiguration(null, null, null, "requestidkey", null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, "requestidkey", null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -80,7 +80,7 @@ public void ShouldSetRequestId() [Fact] public void ShouldSetAspDotNetRequestId() { - var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) diff --git a/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs index ad981a59b..f020707be 100644 --- a/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs +++ b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs @@ -16,7 +16,7 @@ public void ShouldNotBuild() { var configRepo = new Mock(); configRepo.Setup(x => x.Get()) - .Returns(new OkResponse(new InternalConfiguration(null, null, null, null, null, null, null, null, null))); + .Returns(new OkResponse(new InternalConfiguration(null, null, null, null, null, null, null, null, null, null))); var services = new ServiceCollection(); services.AddSingleton(configRepo.Object); var sp = services.BuildServiceProvider(); @@ -31,7 +31,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))); + .Returns(new OkResponse(new InternalConfiguration(null, null, serviceProviderConfig, null, null, null, null, null, null, null))); var services = new ServiceCollection(); services.AddSingleton(configRepo.Object); services.AddSingleton(client.Object); diff --git a/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs index b14534afc..fe92101c3 100644 --- a/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Eureka { - public class EurekaServiceDiscoveryProviderTests + public class EurekaServiceDiscoveryProviderTests : UnitTest { private readonly _Eureka_ _provider; private readonly Mock _client; diff --git a/test/Ocelot.UnitTests/FileUnitTest.cs b/test/Ocelot.UnitTests/FileUnitTest.cs index 1b81b7e8b..34c9de0a7 100644 --- a/test/Ocelot.UnitTests/FileUnitTest.cs +++ b/test/Ocelot.UnitTests/FileUnitTest.cs @@ -16,12 +16,17 @@ protected FileUnitTest(string folder) { folder ??= TestID; Directory.CreateDirectory(folder); - _folders = [folder]; + _folders = new() { folder }; _primaryConfigFileName = Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile); _globalConfigFileName = Path.Combine(folder, ConfigurationBuilderExtensions.GlobalConfigFile); _environmentConfigFileName = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, EnvironmentName())); - _files = [_primaryConfigFileName, _globalConfigFileName, _environmentConfigFileName]; + _files = new() + { + _primaryConfigFileName, + _globalConfigFileName, + _environmentConfigFileName, + }; } protected virtual string EnvironmentName() => TestID; diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs index 8cfcca4fd..d2a1cda2d 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Headers { - public class AddHeadersToRequestClaimToThingTests + public class AddHeadersToRequestClaimToThingTests : UnitTest { private readonly AddHeadersToRequest _addHeadersToRequest; private readonly Mock _parser; diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs index d4c75ed7e..0b30141cf 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Headers { - public class AddHeadersToRequestPlainTests + public class AddHeadersToRequestPlainTests : UnitTest { private readonly AddHeadersToRequest _addHeadersToRequest; private HttpContext _context; diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs index 4563965bb..b0a6e0514 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Headers { - public class AddHeadersToResponseTests + public class AddHeadersToResponseTests : UnitTest { private readonly IAddHeadersToResponse _adder; private readonly Mock _placeholders; diff --git a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs index 7188500fd..9e2eec2b1 100644 --- a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Headers { - public class ClaimsToHeadersMiddlewareTests + public class ClaimsToHeadersMiddlewareTests : UnitTest { private readonly Mock _addHeaders; private Response _downstreamRoute; diff --git a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs index 51fe342b1..18021084d 100644 --- a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Headers { - public class HttpContextRequestHeaderReplacerTests + public class HttpContextRequestHeaderReplacerTests : UnitTest { private HttpContext _context; private List _fAndRs; diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs index d9441462b..f61c72896 100644 --- a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Headers { - public class HttpHeadersTransformationMiddlewareTests + public class HttpHeadersTransformationMiddlewareTests : UnitTest { private readonly Mock _preReplacer; private readonly Mock _postReplacer; diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs index 82e98c0f2..8ccef9447 100644 --- a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Headers { - public class HttpResponseHeaderReplacerTests + public class HttpResponseHeaderReplacerTests : UnitTest { private DownstreamResponse _response; private readonly Placeholders _placeholders; diff --git a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs index 12df11575..f39192295 100644 --- a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs +++ b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Headers { - public class RemoveHeadersTests + public class RemoveHeadersTests : UnitTest { private List
_headers; private readonly Ocelot.Headers.RemoveOutputHeaders _removeOutputHeaders; diff --git a/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs index ddcf726ac..9c5f7bf10 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Infrastructure { - public class ClaimParserTests + public class ClaimParserTests : UnitTest { private readonly IClaimsParser _claimsParser; private readonly List _claims; diff --git a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs index 191cc0499..17e21a622 100644 --- a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Infrastructure { - public class HttpDataRepositoryTests + public class HttpDataRepositoryTests : UnitTest { private readonly HttpContext _httpContext; private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs b/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs deleted file mode 100644 index 8b1378917..000000000 --- a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs index f580a746a..b94774bbe 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Infrastructure { - public class ScopesAuthorizerTests + public class ScopesAuthorizerTests : UnitTest { private readonly ScopesAuthorizer _authorizer; public Mock _parser; diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs new file mode 100644 index 000000000..31f76c4e3 --- /dev/null +++ b/test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs @@ -0,0 +1,162 @@ +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes; +using Ocelot.Provider.Kubernetes.Interfaces; +using Ocelot.Values; + +namespace Ocelot.UnitTests.Kubernetes; + +[Trait("Feat", "1967")] +public sealed class KubeServiceBuilderTests +{ + private readonly Mock factory; + private readonly Mock serviceCreator; + private readonly Mock logger; + private KubeServiceBuilder sut; + + public KubeServiceBuilderTests() + { + factory = new(); + serviceCreator = new(); + logger = new(); + } + + private void Arrange() + { + factory.Setup(x => x.CreateLogger()) + .Returns(logger.Object) + .Verifiable(); + logger.Setup(x => x.LogDebug(It.IsAny>())) + .Verifiable(); + sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + public void Cstor_NullArgs_ThrownException(bool isFactory, bool isServiceCreator) + { + // Arrange + var arg1 = isFactory ? factory.Object : null; + var arg2 = isServiceCreator ? serviceCreator.Object : null; + + // Act, Assert + Assert.Throws( + arg1 is null ? "factory" : arg2 is null ? "serviceCreator" : string.Empty, + () => sut = new KubeServiceBuilder(arg1, arg2)); + } + + [Fact] + public void Cstor_NotNullArgs_ObjCreated() + { + // Arrange + factory.Setup(x => x.CreateLogger()).Verifiable(); + + // Act + sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object); + + // Assert + Assert.NotNull(sut); + factory.Verify(x => x.CreateLogger(), Times.Once()); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + public void BuildServices_NullArgs_ThrownException(bool isConfiguration, bool isEndpoint) + { + // Arrange + var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null; + var arg2 = isEndpoint ? new EndpointsV1() : null; + Arrange(); + + // Act, Assert + Assert.Throws( + arg1 is null ? "configuration" : arg2 is null ? "endpoint" : string.Empty, + () => _ = sut.BuildServices(arg1, arg2)); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void BuildServices_WithSubsets_SelectedManyServicesPerSubset(int subsetCount) + { + // Arrange + var configuration = new KubeRegistryConfiguration(); + var endpoint = new EndpointsV1(); + for (int i = 1; i <= subsetCount; i++) + { + var subset = new EndpointSubsetV1(); + subset.Addresses.Add(new() { NodeName = "subset" + i, Hostname = i.ToString() }); + endpoint.Subsets.Add(subset); + } + + serviceCreator.Setup(x => x.Create(configuration, endpoint, It.IsAny())) + .Returns((c, e, s) => + { + var item = s.Addresses[0]; + int count = int.Parse(item.Hostname); + var list = new List(count); + while (count > 0) + { + var id = count--.ToString(); + list.Add(new Service($"{item.NodeName}-service{id}", null, id, id, null)); + } + + return list; + }); + var many = endpoint.Subsets.Sum(s => int.Parse(s.Addresses[0].Hostname)); + Arrange(); + + // Act + var actual = sut.BuildServices(configuration, endpoint); + + // Assert + Assert.NotNull(actual); + var l = actual.ToList(); + Assert.Equal(many, l.Count); + serviceCreator.Verify(x => x.Create(configuration, endpoint, It.IsAny()), + Times.Exactly(endpoint.Subsets.Count)); + logger.Verify(x => x.LogDebug(It.IsAny>()), + Times.Once()); + } + + [Theory] + [InlineData(false, false, false, false, "K8s '?:?:?' endpoint: Total built 0 services.")] + [InlineData(false, false, false, true, "K8s '?:?:?' endpoint: Total built 0 services.")] + [InlineData(false, false, true, false, "K8s '?:?:?' endpoint: Total built 0 services.")] + [InlineData(false, false, true, true, "K8s '?:?:Name' endpoint: Total built 0 services.")] + [InlineData(false, true, true, true, "K8s '?:ApiVersion:Name' endpoint: Total built 0 services.")] + [InlineData(true, true, true, true, "K8s 'Kind:ApiVersion:Name' endpoint: Total built 0 services.")] + public void BuildServices_WithEndpoint_LogDebug(bool hasKind, bool hasApiVersion, bool hasMetadata, bool hasMetadataName, string message) + { + // Arrange + var configuration = new KubeRegistryConfiguration(); + var endpoint = new EndpointsV1() + { + Kind = hasKind ? nameof(EndpointsV1.Kind) : null, + ApiVersion = hasApiVersion ? nameof(EndpointsV1.ApiVersion) : null, + Metadata = hasMetadata ? new() + { + Name = hasMetadataName ? nameof(ObjectMetaV1.Name) : null, + } : null, + }; + Arrange(); + string actualMesssage = null; + logger.Setup(x => x.LogDebug(It.IsAny>())) + .Callback>(f => actualMesssage = f.Invoke()); + + // Act + var actual = sut.BuildServices(configuration, endpoint); + + // Assert + Assert.NotNull(actual); + logger.Verify(x => x.LogDebug(It.IsAny>()), + Times.Once()); + Assert.NotNull(actualMesssage); + Assert.Equal(message, actualMesssage); + } +} diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs new file mode 100644 index 000000000..a8794ff87 --- /dev/null +++ b/test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs @@ -0,0 +1,150 @@ +using KubeClient.Models; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes; + +namespace Ocelot.UnitTests.Kubernetes; + +[Trait("Feat", "1967")] +public sealed class KubeServiceCreatorTests +{ + private readonly Mock factory; + private readonly Mock logger; + private KubeServiceCreator sut; + + public KubeServiceCreatorTests() + { + factory = new(); + logger = new(); + } + + private void Arrange() + { + factory.Setup(x => x.CreateLogger()) + .Returns(logger.Object) + .Verifiable(); + logger.Setup(x => x.LogDebug(It.IsAny>())) + .Verifiable(); + sut = new KubeServiceCreator(factory.Object); + } + + [Fact] + public void Cstor_NullArg_ThrownException() + { + // Arrange, Act, Assert + Assert.Throws("factory", + () => sut = new KubeServiceCreator(null)); + } + + [Fact] + public void Cstor_NotNullArg_ObjCreated() + { + // Arrange + factory.Setup(x => x.CreateLogger()).Verifiable(); + + // Act + sut = new KubeServiceCreator(factory.Object); + + // Assert + Assert.NotNull(sut); + factory.Verify(x => x.CreateLogger(), Times.Once()); + } + + [Theory] + [InlineData(false, true, true)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + public void Create_NullArgs_ReturnedEmpty(bool isConfiguration, bool isEndpoint, bool isSubset) + { + // Arrange + var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null; + var arg2 = isEndpoint ? new EndpointsV1() : null; + var arg3 = isSubset ? new EndpointSubsetV1() : null; + Arrange(); + + // Act + var actual = sut.Create(arg1, arg2, arg3); + + // Assert + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact(DisplayName = "Create: With empty args -> No exceptions during creation")] + public void Create_NotNullButEmptyArgs_CreatedEmptyService() + { + // Arrange + var arg1 = new KubeRegistryConfiguration() + { + KubeNamespace = nameof(KubeServiceCreatorTests), + KeyOfServiceInK8s = nameof(Create_NotNullButEmptyArgs_CreatedEmptyService), + }; + var arg2 = new EndpointsV1(); + var arg3 = new EndpointSubsetV1(); + arg3.Addresses.Add(new()); + arg2.Subsets.Add(arg3); + Arrange(); + + // Act + var actual = sut.Create(arg1, arg2, arg3); + + // Assert + Assert.NotNull(actual); + Assert.NotEmpty(actual); + var actualService = actual.SingleOrDefault(); + Assert.NotNull(actualService); + Assert.Null(actualService.Name); + } + + [Fact] + public void Create_ValidArgs_HappyPath() + { + // Arrange + var arg1 = new KubeRegistryConfiguration() + { + KubeNamespace = nameof(KubeServiceCreatorTests), + KeyOfServiceInK8s = nameof(Create_ValidArgs_HappyPath), + Scheme = "happy", //nameof(HttpScheme.Http), + }; + var arg2 = new EndpointsV1() + { + ApiVersion = "v1", + Metadata = new() + { + Namespace = nameof(KubeServiceCreatorTests), + Name = nameof(Create_ValidArgs_HappyPath), + Uid = Guid.NewGuid().ToString(), + }, + }; + var arg3 = new EndpointSubsetV1(); + arg3.Addresses.Add(new() + { + Ip = "8.8.8.8", + NodeName = "google", + Hostname = "dns.google", + }); + var ports = new List + { + new() { Name = nameof(HttpScheme.Http), Port = 80 }, + new() { Name = "happy", Port = 888 }, + }; + arg3.Ports.AddRange(ports); + arg2.Subsets.Add(arg3); + Arrange(); + + // Act + var actual = sut.Create(arg1, arg2, arg3); + + // Assert + Assert.NotNull(actual); + Assert.NotEmpty(actual); + var service = actual.SingleOrDefault(); + Assert.NotNull(service); + Assert.Equal(nameof(Create_ValidArgs_HappyPath), service.Name); + Assert.Equal("happy", service.HostAndPort.Scheme); + Assert.Equal(888, service.HostAndPort.DownstreamPort); + Assert.Equal("8.8.8.8", service.HostAndPort.DownstreamHost); + logger.Verify(x => x.LogDebug(It.IsAny>()), + Times.Once()); + } +} diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs index 49e91fe6e..213a25f65 100644 --- a/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; +using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.UnitTests.Kubernetes @@ -21,10 +22,11 @@ public class KubeTests : IDisposable private readonly string _kubeHost; private readonly string _fakekubeServiceDiscoveryUrl; private List _services; + private string _receivedToken; private readonly Mock _factory; private readonly Mock _logger; - private string _receivedToken; private readonly IKubeApiClient _clientFactory; + private readonly Mock _serviceBuilder; public KubeTests() { @@ -33,8 +35,8 @@ public KubeTests() _port = 5567; _kubeHost = "localhost"; _fakekubeServiceDiscoveryUrl = $"{Uri.UriSchemeHttp}://{_kubeHost}:{_port}"; - _endpointEntries = new EndpointsV1(); - _factory = new Mock(); + _endpointEntries = new(); + _factory = new(); var option = new KubeClientOptions { @@ -45,19 +47,21 @@ public KubeTests() }; _clientFactory = KubeApiClient.Create(option); - _logger = new Mock(); + _logger = new(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); var config = new KubeRegistryConfiguration { KeyOfServiceInK8s = _serviceName, KubeNamespace = _namespaces, }; - _provider = new Kube(config, _factory.Object, _clientFactory); + _serviceBuilder = new(); + _provider = new Kube(config, _factory.Object, _clientFactory, _serviceBuilder.Object); } [Fact] public void Should_return_service_from_k8s() { + // Arrange var token = "Bearer txpc696iUhbVoudg164r93CxDTrKRVWG"; var endPointEntryOne = new EndpointsV1 { @@ -65,6 +69,7 @@ public void Should_return_service_from_k8s() ApiVersion = "1.0", Metadata = new ObjectMetaV1 { + Name = nameof(Should_return_service_from_k8s), Namespace = "dev", }, }; @@ -79,13 +84,17 @@ public void Should_return_service_from_k8s() Port = 80, }); endPointEntryOne.Subsets.Add(endpointSubsetV1); + _serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) + .Returns(new Service[] { new(nameof(Should_return_service_from_k8s), new("localhost", 80), string.Empty, string.Empty, new string[0]) }); + GivenThereIsAFakeKubeServiceDiscoveryProvider(_fakekubeServiceDiscoveryUrl, _serviceName, _namespaces); + GivenTheServicesAreRegisteredWithKube(endPointEntryOne); + + // Act + WhenIGetTheServices(); - this.Given(x => GivenThereIsAFakeKubeServiceDiscoveryProvider(_fakekubeServiceDiscoveryUrl, _serviceName, _namespaces)) - .And(x => GivenTheServicesAreRegisteredWithKube(endPointEntryOne)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(1)) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); + // Assert + ThenTheCountIs(1); + ThenTheTokenIs(token); } private void ThenTheTokenIs(string token) diff --git a/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs index 2f32b562d..d3c0b9967 100644 --- a/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Kubernetes { - public class OcelotBuilderExtensionsTests + public class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs b/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs index d041288f6..794589bc3 100644 --- a/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Kubernetes { - public class PollKubeTests + public class PollKubeTests : UnitTest { private readonly int _delay; private PollKube _provider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs index 601e837e9..682ea9d41 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class CookieStickySessionsCreatorTests + public class CookieStickySessionsCreatorTests : UnitTest { private readonly CookieStickySessionsCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs index 615218deb..71c4d0517 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class CookieStickySessionsTests + public class CookieStickySessionsTests : UnitTest { private readonly CookieStickySessions _stickySessions; private readonly Mock _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs index 6631051fc..deb25a12e 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class DelegateInvokingLoadBalancerCreatorTests + public class DelegateInvokingLoadBalancerCreatorTests : UnitTest { private DelegateInvokingLoadBalancerCreator _creator; private Func _creatorFunc; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs index e94cfc5b6..e56fa8f7b 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LeastConnectionCreatorTests + public class LeastConnectionCreatorTests : UnitTest { private readonly LeastConnectionCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs index 2766f58ab..368e866e6 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LeastConnectionTests + public class LeastConnectionTests : UnitTest { private ServiceHostAndPort _hostAndPort; private Response _result; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs index 5eb21c6d7..616168548 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LoadBalancerFactoryTests + public class LoadBalancerFactoryTests : UnitTest { private DownstreamRoute _route; private readonly LoadBalancerFactory _factory; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs index 216bda84e..fa0b835ff 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LoadBalancerHouseTests + public class LoadBalancerHouseTests : UnitTest { private DownstreamRoute _route; private ILoadBalancer _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs index 411c43fd4..fd46e9a2a 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LoadBalancerMiddlewareTests + public class LoadBalancerMiddlewareTests : UnitTest { private readonly Mock _loadBalancerHouse; private readonly Mock _loadBalancer; @@ -131,7 +131,7 @@ private void WhenICallTheMiddleware() private void GivenTheConfigurationIs(ServiceProviderConfiguration config) { _config = config; - var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null); + var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null, null); _httpContext.Items.SetIInternalConfiguration(configuration); } diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs index df478ec24..dadf453e1 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class NoLoadBalancerCreatorTests + public class NoLoadBalancerCreatorTests : UnitTest { private readonly NoLoadBalancerCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs index 5e562352a..e1490e898 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class NoLoadBalancerTests + public class NoLoadBalancerTests : UnitTest { private readonly List _services; private NoLoadBalancer _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs index 2b3d00d8a..13f5b6622 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class RoundRobinCreatorTests + public class RoundRobinCreatorTests : UnitTest { private readonly RoundRobinCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs index 78196c13e..af55d65aa 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class RoundRobinTests + public class RoundRobinTests : UnitTest { private readonly RoundRobin _roundRobin; private readonly List _services; diff --git a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs index d7ac54b64..94d3da1c7 100644 --- a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs +++ b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Logging { - public class OcelotDiagnosticListenerTests + public class OcelotDiagnosticListenerTests : UnitTest { private readonly OcelotDiagnosticListener _listener; private readonly Mock _factory; diff --git a/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs index 4049e08aa..e6db74c33 100644 --- a/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Middleware { - public class BaseUrlFinderTests + public class BaseUrlFinderTests : UnitTest { private BaseUrlFinder _baseUrlFinder; private IConfiguration _config; diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs index f7f41ae6a..7d2e4b75a 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs @@ -12,7 +12,7 @@ namespace Ocelot.UnitTests.Middleware { - public class OcelotPipelineExtensionsTests + public class OcelotPipelineExtensionsTests : UnitTest { private ApplicationBuilder _builder; private RequestDelegate _handlers; diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs index 0cd2147c3..2948b70a6 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Middleware { - public class OcelotPiplineBuilderTests + public class OcelotPiplineBuilderTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs index b9a6c91b3..7a1c8e914 100644 --- a/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class DefinedAggregatorProviderTests + public class DefinedAggregatorProviderTests : UnitTest { private ServiceLocatorDefinedAggregatorProvider _provider; private Response _aggregator; diff --git a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs index 633fae43b..b94c247ff 100644 --- a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class MultiplexingMiddlewareTests + public class MultiplexingMiddlewareTests : UnitTest { private MultiplexingMiddleware _middleware; private Ocelot.DownstreamRouteFinder.DownstreamRouteHolder _downstreamRoute; @@ -61,14 +61,14 @@ public void should_not_multiplex() [Fact] [Trait("Bug", "1396")] - public void CreateThreadContext_CopyUser_ToTarget() + public async Task CreateThreadContextAsync_CopyUser_ToTarget() { // Arrange - GivenUser("test", "Copy", nameof(CreateThreadContext_CopyUser_ToTarget)); + GivenUser("test", "Copy", nameof(CreateThreadContextAsync_CopyUser_ToTarget)); // Act - var method = _middleware.GetType().GetMethod("CreateThreadContext", BindingFlags.NonPublic | BindingFlags.Static); - var actual = (HttpContext)method.Invoke(_middleware, [_httpContext]); + var method = _middleware.GetType().GetMethod("CreateThreadContextAsync", BindingFlags.NonPublic | BindingFlags.Instance); + var actual = await (Task)method.Invoke(_middleware, new object[] { _httpContext }); // Assert AssertUsers(actual); @@ -188,8 +188,8 @@ public async Task Should_Create_As_Many_Contexts_As_Routes_And_Map_Is_Called_Onc ItExpr.IsAny(), ItExpr.Is>(list => list.Count == routesCount) ); - } - + } + [Fact] [Trait("PR", "1826")] public async Task Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route() @@ -212,7 +212,30 @@ public async Task Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route() ItExpr.IsAny(), ItExpr.IsAny(), ItExpr.IsAny>()); - } + } + + [Theory] + [Trait("Bug", "2039")] + [InlineData(1)] // Times.Never() + [InlineData(2)] // Times.Exactly(2) + [InlineData(3)] // Times.Exactly(3) + [InlineData(4)] // Times.Exactly(4) + public async Task Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests(int numberOfRoutes) + { + // Arrange + var mock = MockMiddlewareFactory(null, null); + GivenUser("test", "Invoke", nameof(Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests)); + GivenTheFollowing(GivenDefaultRoute(numberOfRoutes)); + + // Act + await WhenIMultiplex(); + + // Assert + mock.Protected().Verify>("CloneRequestBodyAsync", + numberOfRoutes > 1 ? Times.Exactly(numberOfRoutes) : Times.Never(), + ItExpr.IsAny(), + ItExpr.IsAny()); + } [Fact] [Trait("PR", "1826")] @@ -318,13 +341,11 @@ private static Route GivenRoutesWithAggregator() b.WithDownstreamRoute(route2); b.WithDownstreamRoute(route3); - b.WithAggregateRouteConfig( - [ - new AggregateRouteConfig - { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, - new AggregateRouteConfig - { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" } - ]); + b.WithAggregateRouteConfig(new() + { + new AggregateRouteConfig { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, + new AggregateRouteConfig { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, + }); b.WithAggregator("TestAggregator"); diff --git a/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs index 7db1d8faf..15a229573 100644 --- a/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class ResponseAggregatorFactoryTests + public class ResponseAggregatorFactoryTests : UnitTest { private readonly InMemoryResponseAggregatorFactory _factory; private readonly Mock _provider; diff --git a/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs index 45f7f2d92..fafda9bfd 100644 --- a/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class SimpleJsonResponseAggregatorTests + public class SimpleJsonResponseAggregatorTests : UnitTest { private readonly SimpleJsonResponseAggregator _aggregator; private List _downstreamContexts; diff --git a/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs index a960dc119..4a0349795 100644 --- a/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class UserDefinedResponseAggregatorTests + public class UserDefinedResponseAggregatorTests : UnitTest { private readonly UserDefinedResponseAggregator _aggregator; private readonly Mock _provider; diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index d9af1c858..5cab0b487 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -16,7 +16,8 @@ false ..\..\codeanalysis.ruleset True - 1591;CS0618 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 full @@ -26,12 +27,12 @@ - + @@ -74,7 +75,7 @@ - + diff --git a/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs deleted file mode 100644 index f968fb5ee..000000000 --- a/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs +++ /dev/null @@ -1,255 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Logging; -using Ocelot.Provider.Polly; -using Polly; -using Polly.Wrap; -using System.Reflection; -using Ocelot.Provider.Polly.v7; - -namespace Ocelot.UnitTests.Polly; - -public class PollyPoliciesDelegatingHandlerTests -{ - private readonly Mock> _pollyQoSProviderMock; - private readonly Mock _contextAccessorMock; - private readonly PollyPoliciesDelegatingHandler _sut; - - public PollyPoliciesDelegatingHandlerTests() - { - _pollyQoSProviderMock = new Mock>(); - - var loggerFactoryMock = new Mock(); - var loggerMock = new Mock(); - _contextAccessorMock = new Mock(); - - loggerFactoryMock.Setup(x => x.CreateLogger()) - .Returns(loggerMock.Object); - loggerMock.Setup(x => x.LogError(It.IsAny(), It.IsAny())); - - _sut = new PollyPoliciesDelegatingHandler(DownstreamRouteFactory(), _contextAccessorMock.Object, loggerFactoryMock.Object); - } - - [Fact] - public async void SendAsync_OnePolicy_NoWrapping() - { - // Arrange - var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); - fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_OnePolicy_NoWrapping)); - - MethodInfo method = null; - var onePolicy = new Mock>(); - onePolicy.Setup(x => x.ExecuteAsync(It.IsAny>>())) - .Callback((IInvocation x) => method = x.Method) - .ReturnsAsync(fakeResponse); - - _pollyQoSProviderMock.Setup(x => x.GetPollyPolicyWrapper(It.IsAny())) - .Returns(new PollyPolicyWrapper(onePolicy.Object)); - - var httpContext = new Mock(); - httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSProvider))) - .Returns(_pollyQoSProviderMock.Object); - - _contextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext.Object); - - // Act - var actual = await InvokeAsync("SendAsync"); - - // Assert - ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_OnePolicy_NoWrapping)); - method.DeclaringType.Name.ShouldBe("IAsyncPolicy`1"); - method.DeclaringType.ShouldNotBeOfType(); - } - - [Fact] - public async void SendAsync_TwoPolicies_HaveWrapped() - { - // Arrange - var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); - fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_TwoPolicies_HaveWrapped)); - - var policy1 = new FakeAsyncPolicy("Policy1", fakeResponse); - var policy2 = new FakeAsyncPolicy("Policy2", fakeResponse) - { - IsLast = true, - }; - - _pollyQoSProviderMock.Setup(x => x.GetPollyPolicyWrapper(It.IsAny())) - .Returns(new PollyPolicyWrapper(policy1, policy2)); - - var httpContext = new Mock(); - httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSProvider))) - .Returns(_pollyQoSProviderMock.Object); - - _contextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext.Object); - - // Act - var actual = await InvokeAsync("SendAsync"); - - // Assert - ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_TwoPolicies_HaveWrapped)); - ShouldBeWrappedBy(policy1, typeof(AsyncPolicyWrap).FullName); - ShouldBeWrappedBy(policy2, typeof(AsyncPolicy).FullName); - } - - private static DownstreamRoute DownstreamRouteFactory() - { - var options = new QoSOptionsBuilder() - .WithTimeoutValue(100) - .WithExceptionsAllowedBeforeBreaking(2) - .WithDurationOfBreak(200) - .Build(); - - var upstreamPath = new UpstreamPathTemplateBuilder() - .WithTemplate("/") - .WithContainsQueryString(false) - .WithPriority(1) - .WithOriginalValue("/").Build(); - - var route = new DownstreamRouteBuilder() - .WithQosOptions(options) - .WithUpstreamPathTemplate(upstreamPath).Build(); - - return route; - } - - private static void ShouldHaveXunitHeaderWithNoContent(HttpResponseMessage actual, string headerName) - { - actual.ShouldNotBeNull(); - actual.StatusCode.ShouldBe(HttpStatusCode.NoContent); - actual.Headers.GetValues("X-Xunit").ShouldContain(headerName); - } - - private static void ShouldBeWrappedBy(FakeAsyncPolicy policy, string wrapperName) - { - policy.Called.ShouldBeTrue(); - policy.Times.ShouldBe(1); - policy.Method.ShouldNotBeNull(); - policy.Target.ShouldNotBeNull(); - policy.Method.DeclaringType?.DeclaringType.ShouldNotBeNull(); - policy.Method.DeclaringType.DeclaringType.FullName.ShouldContain(wrapperName); - policy.Target.ToString().ShouldContain(wrapperName); - } - - private async Task InvokeAsync(string methodName) - { - var m = typeof(PollyPoliciesDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); - var task = (Task)m.Invoke(_sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); - var actual = await task; - return actual; - } - - internal class FakeAsyncPolicy : AsyncPolicy, IAsyncPolicy - where TResult : class - { - public object Result { get; private set; } - public string Name { get; private set; } - - public int Times { get; protected set; } - public bool Called => Times > 0; - public MethodInfo Method { get; protected set; } - public object Target { get; protected set; } - - public bool IsLast { get; set; } - - public FakeAsyncPolicy(string name, object result) - { - Name = name; - Result = result; - } - - protected override async Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, - bool continueOnCapturedContext) - { - Times++; - Method = action.Method; - Target = action.Target; - - if (IsLast) - { - var r = Result?.GetType() == typeof(TResult) - ? (TResult)Result - : Activator.CreateInstance(); - return r; - } - - var result = await action(context, cancellationToken); - return result; - } - - public new IAsyncPolicy WithPolicyKey(string policyKey) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, IDictionary contextData) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, Context context) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, Context context) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, IDictionary contextData) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, Context context) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, Context context) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - } -} diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs deleted file mode 100644 index 8c08a46e0..000000000 --- a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs +++ /dev/null @@ -1,245 +0,0 @@ -using Ocelot.Configuration.Builder; -using Ocelot.Logging; -using Ocelot.Provider.Polly; -using Ocelot.Provider.Polly.v7; -using Polly; -using Polly.CircuitBreaker; -using Polly.Timeout; -using Polly.Wrap; - -namespace Ocelot.UnitTests.Polly; - -public class PollyQoSProviderTests -{ - [Fact] - public void Should_build() - { - var options = new QoSOptionsBuilder() - .WithTimeoutValue(100) - .WithExceptionsAllowedBeforeBreaking(1) - .WithDurationOfBreak(200) - .Build(); - var route = new DownstreamRouteBuilder().WithQosOptions(options) - .Build(); - var factory = new Mock(); - var pollyQoSProvider = new PollyQoSProvider(factory.Object); - var policy = pollyQoSProvider.GetPollyPolicyWrapper(route).ShouldNotBeNull() - .AsyncPollyPolicy.ShouldNotBeNull(); - policy.ShouldNotBeNull(); - } - - [Fact] - public void should_build_and_wrap_contains_two_policies() - { - var pollyQosProvider = PollyQoSProviderFactory(); - var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); - var policy = pollyPolicyWrapper.AsyncPollyPolicy; - - if (policy is AsyncPolicyWrap policyWrap) - { - policyWrap.ShouldNotBeNull(); - var policies = policyWrap.GetPolicies().ToList(); - - policies.Count.ShouldBe(2); - var circuitBreakerFound = false; - var timeoutPolicyFound = false; - - foreach(var currentPolicy in policies) - { - currentPolicy.ShouldNotBeNull(); - var convertedPolicy = (IAsyncPolicy)currentPolicy; - - switch (convertedPolicy) - { - case AsyncCircuitBreakerPolicy: - circuitBreakerFound = true; - continue; - case AsyncTimeoutPolicy: - timeoutPolicyFound = true; - break; - } - } - - Assert.True(circuitBreakerFound); - Assert.True(timeoutPolicyFound); - - return; - } - - Assert.Fail("policy is not AsyncPolicyWrap"); - } - - [Fact] - public void should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() - { - var pollyQosProvider = PollyQoSProviderFactory(); - var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider, true); - var policy = pollyPolicyWrapper.AsyncPollyPolicy; - - if (policy is AsyncTimeoutPolicy convertedPolicy) - { - convertedPolicy.ShouldNotBeNull(); - return; - } - - Assert.Fail("policy is not AsyncTimeoutPolicy"); - } - - [Fact] - public void should_return_same_circuit_breaker_for_given_route() - { - var pollyQosProvider = PollyQoSProviderFactory(); - var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); - var pollyPolicyWrapper2 = PolicyWrapperFactory("/", pollyQosProvider); - pollyPolicyWrapper.ShouldBe(pollyPolicyWrapper2); - } - - [Fact] - public void should_return_different_circuit_breaker_for_two_different_routes() - { - var pollyQosProvider = PollyQoSProviderFactory(); - var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); - var pollyPolicyWrapper2 = PolicyWrapperFactory("/test", pollyQosProvider); - pollyPolicyWrapper.ShouldNotBe(pollyPolicyWrapper2); - } - - [Theory] - [InlineData(HttpStatusCode.InternalServerError)] - [InlineData(HttpStatusCode.NotImplemented)] - [InlineData(HttpStatusCode.BadGateway)] - [InlineData(HttpStatusCode.ServiceUnavailable)] - [InlineData(HttpStatusCode.GatewayTimeout)] - [InlineData(HttpStatusCode.HttpVersionNotSupported)] - [InlineData(HttpStatusCode.VariantAlsoNegotiates)] - [InlineData(HttpStatusCode.InsufficientStorage)] - [InlineData(HttpStatusCode.LoopDetected)] - public async Task should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(errorCode); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - } - - [Fact] - public async Task should_not_throw_broken_circuit_exception_if_status_code_ok() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.OK); - Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - } - - [Fact(Skip = "TODO", DisplayName = "TODO " + nameof(should_throw_and_before_delay_should_not_allow_requests))] - [Trait("TODO", "Fix after the release")] - public async Task should_throw_and_before_delay_should_not_allow_requests() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - - await Task.Delay(200); - - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - } - - [Fact] - public async Task should_throw_but_after_delay_should_allow_one_more_internal_server_error() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - - await Task.Delay(600); - - Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - } - - [Fact] - public async Task should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - - await Task.Delay(600); - - Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - } - - [Fact] - public async Task should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - - await Task.Delay(600); - - var response2 = new HttpResponseMessage(HttpStatusCode.OK); - Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response2))).StatusCode); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - } - - private PollyQoSProvider PollyQoSProviderFactory() - { - var factory = new Mock(); - factory.Setup(x => x.CreateLogger()) - .Returns(new Mock().Object); - - var pollyQoSProvider = new PollyQoSProvider(factory.Object); - return pollyQoSProvider; - } - - private static PollyPolicyWrapper PolicyWrapperFactory(string routeTemplate, PollyQoSProvider pollyQoSProvider, bool inactiveExceptionsAllowedBeforeBreaking = false) - { - var options = new QoSOptionsBuilder() - .WithTimeoutValue(5000) - .WithExceptionsAllowedBeforeBreaking(inactiveExceptionsAllowedBeforeBreaking ? 0 : 2) - .WithDurationOfBreak(300) - .Build(); - - var upstreamPath = new UpstreamPathTemplateBuilder() - .WithTemplate(routeTemplate) - .WithContainsQueryString(false) - .WithPriority(1) - .WithOriginalValue(routeTemplate).Build(); - - var route = new DownstreamRouteBuilder() - .WithQosOptions(options) - .WithUpstreamPathTemplate(upstreamPath).Build(); - - var pollyPolicyWrapper = pollyQoSProvider.GetPollyPolicyWrapper(route).ShouldNotBeNull(); - pollyPolicyWrapper.ShouldNotBeNull(); - pollyPolicyWrapper.AsyncPollyPolicy.ShouldNotBeNull(); - - return pollyPolicyWrapper; - } -} diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs index 1099a2281..6a80d2bea 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs @@ -2,6 +2,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Polly; +using Polly; using Polly.CircuitBreaker; using Polly.Registry; using Polly.Testing; @@ -12,37 +13,109 @@ namespace Ocelot.UnitTests.Polly; public class PollyQoSResiliencePipelineProviderTests { [Fact] - public void Should_build() + public void ShouldBuild() { + // Arrange var options = new QoSOptionsBuilder() - .WithTimeoutValue(1000) // 1s, minimum required by Polly + .WithTimeoutValue(1000) // 10ms, minimum required by Polly .WithExceptionsAllowedBeforeBreaking(2) // 2 is the minimum required by Polly - .WithDurationOfBreak(500) // 0.5s, minimum required by Polly + .WithDurationOfBreak(QoSOptions.LowBreakDuration + 1) // 0.5s, minimum required by Polly .Build(); + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .Build(); + var loggerFactoryMock = new Mock(); + var registry = new ResiliencePipelineRegistry(); + var provider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, registry); + + // Act + var resiliencePipeline = provider.GetResiliencePipeline(route); + // Assert + resiliencePipeline.ShouldNotBeNull(); + resiliencePipeline.ShouldBeOfType>(); + resiliencePipeline.ShouldNotBe(ResiliencePipeline.Empty); + } + + [Fact] + [Trait("Bug", "2085")] + public void ShouldNotBuild_ReturnedEmpty() + { + // Arrange + var options = new QoSOptionsBuilder().Build(); // empty options var route = new DownstreamRouteBuilder() .WithQosOptions(options) .Build(); + var loggerFactoryMock = new Mock(); + var registry = new ResiliencePipelineRegistry(); + var provider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, registry); + + // Act + var resiliencePipeline = provider.GetResiliencePipeline(route); + + // Assert + resiliencePipeline.ShouldNotBeNull(); + resiliencePipeline.ShouldBeOfType>(); + resiliencePipeline.ShouldBe(ResiliencePipeline.Empty); + } + [Theory] + [Trait("Bug", "2085")] + [InlineData(0, QoSOptions.DefaultBreakDuration)] // default + [InlineData(QoSOptions.LowBreakDuration - 1, QoSOptions.DefaultBreakDuration)] // default + [InlineData(QoSOptions.LowBreakDuration, QoSOptions.DefaultBreakDuration)] // default + [InlineData(QoSOptions.LowBreakDuration + 1, QoSOptions.LowBreakDuration + 1)] // not default, exact + public void ShouldBuild_WithDefaultBreakDuration(int durationOfBreak, int expectedMillisecons) + { + // Arrange + var options = new QoSOptionsBuilder() + .WithTimeoutValue(1000) // 10ms, minimum required by Polly + .WithExceptionsAllowedBeforeBreaking(2) // 2 is the minimum required by Polly + .WithDurationOfBreak(durationOfBreak) // 0.5s, minimum required by Polly + .Build(); + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .Build(); var loggerFactoryMock = new Mock(); - var resiliencePipelineRegistry = new ResiliencePipelineRegistry(); - var pollyQoSResiliencePipelineProvider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, resiliencePipelineRegistry); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + var registry = new ResiliencePipelineRegistry(); + var provider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, registry); + + // Act + var resiliencePipeline = provider.GetResiliencePipeline(route); + + // Assert resiliencePipeline.ShouldNotBeNull(); + resiliencePipeline.ShouldBeOfType>(); + resiliencePipeline.ShouldNotBe(ResiliencePipeline.Empty); + var descriptor = resiliencePipeline.GetPipelineDescriptor(); + descriptor.ShouldNotBeNull(); + descriptor.Strategies.Count.ShouldBe(2); + descriptor.Strategies[0].Options.ShouldBeOfType>(); + descriptor.Strategies[1].Options.ShouldBeOfType(); + var strategyOptions = descriptor.Strategies[0].Options as CircuitBreakerStrategyOptions; + strategyOptions.ShouldNotBeNull(); + strategyOptions.BreakDuration.ShouldBe(TimeSpan.FromMilliseconds(expectedMillisecons)); } [Fact] public void Should_return_same_circuit_breaker_for_given_route() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - var route1 = DownstreamRouteFactory("/"); - var route2 = DownstreamRouteFactory("/"); + // Arrange + var provider = GivenProvider(); + var route1 = GivenDownstreamRoute("/"); + var route2 = GivenDownstreamRoute("/"); - var resiliencePipeline1 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route1); - var resiliencePipeline2 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route2); + // Act + var resiliencePipeline1 = provider.GetResiliencePipeline(route1); + var resiliencePipeline2 = provider.GetResiliencePipeline(route2); + + // Assert resiliencePipeline1.ShouldBe(resiliencePipeline2); - var resiliencePipeline3 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route1); + // Act 2 + var resiliencePipeline3 = provider.GetResiliencePipeline(route1); + + // Assert 2 resiliencePipeline3.ShouldBe(resiliencePipeline1); resiliencePipeline3.ShouldBe(resiliencePipeline2); } @@ -50,46 +123,99 @@ public void Should_return_same_circuit_breaker_for_given_route() [Fact] public void Should_return_different_circuit_breaker_for_two_different_routes() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - var route1 = DownstreamRouteFactory("/"); - var route2 = DownstreamRouteFactory("/test"); + // Arrange + var provider = GivenProvider(); + var route1 = GivenDownstreamRoute("/"); + var route2 = GivenDownstreamRoute("/test"); - var resiliencePipeline1 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route1); - var resiliencePipeline2 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route2); + // Act + var resiliencePipeline1 = provider.GetResiliencePipeline(route1); + var resiliencePipeline2 = provider.GetResiliencePipeline(route2); + // Assert resiliencePipeline1.ShouldNotBe(resiliencePipeline2); } [Fact] - public void Should_build_and_wrap_contains_two_policies() + [Trait("Bug", "2085")] + public void ShouldBuild_ContainsTwoStrategies() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + var pollyQoSResiliencePipelineProvider = GivenProvider(); - var route = DownstreamRouteFactory("/"); + var route = GivenDownstreamRoute("/"); var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); resiliencePipeline.ShouldNotBeNull(); - var resiliencePipelineDescriptor = resiliencePipeline.GetPipelineDescriptor(); - resiliencePipelineDescriptor.ShouldNotBeNull(); - resiliencePipelineDescriptor.Strategies.Count.ShouldBe(2); - resiliencePipelineDescriptor.Strategies[0].Options.ShouldBeOfType(); - resiliencePipelineDescriptor.Strategies[1].Options.ShouldBeOfType>(); + var descriptor = resiliencePipeline.GetPipelineDescriptor(); + descriptor.ShouldNotBeNull(); + descriptor.Strategies.Count.ShouldBe(2); + descriptor.Strategies[0].Options.ShouldBeOfType>(); + descriptor.Strategies[1].Options.ShouldBeOfType(); } [Fact] public void Should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/", true); // get route with 0 exceptions allowed before breaking - // get route with 0 exceptions allowed before breaking - var route = DownstreamRouteFactory("/", true); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + // Act + var resiliencePipeline = provider.GetResiliencePipeline(route); + var descriptor = resiliencePipeline.GetPipelineDescriptor(); + + // Assert resiliencePipeline.ShouldNotBeNull(); + descriptor.ShouldNotBeNull(); + descriptor.Strategies.Count.ShouldBe(1); + descriptor.Strategies.Single().Options.ShouldBeOfType(); + } - var resiliencePipelineDescriptor = resiliencePipeline.GetPipelineDescriptor(); - resiliencePipelineDescriptor.ShouldNotBeNull(); - resiliencePipelineDescriptor.Strategies.Count.ShouldBe(1); - resiliencePipelineDescriptor.Strategies.Single().Options.ShouldBeOfType(); + [Fact] + [Trait("Bug", "2085")] + public async Task Should_throw_after_timeout() + { + // Arrange + var provider = GivenProvider(); + const int timeOut = 1000; + var route = GivenDownstreamRoute("/", false, timeOut); + var resiliencePipeline = provider.GetResiliencePipeline(route); + var response = new HttpResponseMessage(HttpStatusCode.OK); + var cancellationTokenSource = new CancellationTokenSource(); + + // Assert + await Assert.ThrowsAsync(async () => + + // Act + await resiliencePipeline.ExecuteAsync(async (cancellationToken) => + { + await Task.Delay(timeOut + 500, cancellationToken); // add 500ms to make sure it's timed out + return response; + }, + cancellationTokenSource.Token)); + } + + [Fact] + [Trait("Bug", "2085")] + public async Task Should_not_throw_before_timeout() + { + // Arrange + var provider = GivenProvider(); + const int timeOut = 1000; + var route = GivenDownstreamRoute("/", false, timeOut); + var resiliencePipeline = provider.GetResiliencePipeline(route); + var response = new HttpResponseMessage(HttpStatusCode.OK); + var cancellationTokenSource = new CancellationTokenSource(); + + // Act + await resiliencePipeline.ExecuteAsync(async cancellationToken => + { + await Task.Delay(timeOut - 100, cancellationToken); // subtract 100ms to make sure it's not timed out + return response; + }, cancellationTokenSource.Token); + + // Assert + Assert.True(response.IsSuccessStatusCode); } [Theory] @@ -104,14 +230,17 @@ public void Should_build_and_contains_one_policy_when_with_exceptions_allowed_be [InlineData(HttpStatusCode.LoopDetected)] public async Task Should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(errorCode); + + // Act await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); } @@ -119,12 +248,13 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Should_not_throw_broken_circuit_exception_if_status_code_ok() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.OK); + + // Act, Assert Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); @@ -133,19 +263,21 @@ public async Task Should_not_throw_broken_circuit_exception_if_status_code_ok() [Fact] public async Task Should_throw_and_before_delay_should_not_allow_requests() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); await Task.Delay(200); + // Act, Assert 2 await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); } @@ -153,39 +285,48 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); await Task.Delay(6000); - Assert.Equal(HttpStatusCode.InternalServerError, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); + // Act 2 + var response2 = await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Assert 2 + Assert.Equal(HttpStatusCode.InternalServerError, response2.StatusCode); } [Fact] public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); await Task.Delay(6000); + // Act, Assert 2 Assert.Equal(HttpStatusCode.InternalServerError, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); + + // Act, Assert 3 await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); } @@ -193,43 +334,45 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); await Task.Delay(10000); + // Act, Assert 2 var response2 = new HttpResponseMessage(HttpStatusCode.OK); Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response2))).StatusCode); + + // Act, Assert 3 await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); } - private static PollyQoSResiliencePipelineProvider PollyQoSResiliencePipelineProviderFactory() + private static PollyQoSResiliencePipelineProvider GivenProvider() { var loggerFactoryMock = new Mock(); loggerFactoryMock .Setup(x => x.CreateLogger()) .Returns(new Mock().Object); - var resiliencePipelineRegistry = new ResiliencePipelineRegistry(); - - var pollyQoSResiliencePipelineProvider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, resiliencePipelineRegistry); - return pollyQoSResiliencePipelineProvider; + var registry = new ResiliencePipelineRegistry(); + return new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, registry); } - private static DownstreamRoute DownstreamRouteFactory(string routeTemplate, bool inactiveExceptionsAllowedBeforeBreaking = false) + private static DownstreamRoute GivenDownstreamRoute(string routeTemplate, bool inactiveExceptionsAllowedBeforeBreaking = false, int timeOut = 10000) { var options = new QoSOptionsBuilder() - .WithTimeoutValue(10000) + .WithTimeoutValue(timeOut) .WithExceptionsAllowedBeforeBreaking(inactiveExceptionsAllowedBeforeBreaking ? 0 : 2) .WithDurationOfBreak(5000) .Build(); @@ -241,11 +384,9 @@ private static DownstreamRoute DownstreamRouteFactory(string routeTemplate, bool .WithOriginalValue(routeTemplate) .Build(); - var route = new DownstreamRouteBuilder() + return new DownstreamRouteBuilder() .WithQosOptions(options) .WithUpstreamPathTemplate(upstreamPath) .Build(); - - return route; } } diff --git a/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs index b1f448c6a..f144fe305 100644 --- a/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs @@ -88,7 +88,7 @@ public async void SendAsync_OnePolicy() private async Task InvokeAsync(string methodName) { var m = typeof(PollyResiliencePipelineDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); - var task = (Task)m.Invoke(_sut, [new HttpRequestMessage(), CancellationToken.None]); + var task = (Task)m.Invoke(_sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); var actual = await task!; return actual; } diff --git a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs index 83c5b320a..5c2c3b794 100644 --- a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.QueryStrings { - public class AddQueriesToRequestTests + public class AddQueriesToRequestTests : UnitTest { private readonly AddQueriesToRequest _addQueriesToRequest; private DownstreamRequest _downstreamRequest; diff --git a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs index 731f3fa0e..bfb0af8aa 100644 --- a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.QueryStrings { - public class ClaimsToQueryStringMiddlewareTests + public class ClaimsToQueryStringMiddlewareTests : UnitTest { private readonly Mock _addQueries; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs deleted file mode 100644 index 0e6c66068..000000000 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.RateLimit; -using Ocelot.RateLimit.Middleware; -using Ocelot.Request.Middleware; - -namespace Ocelot.UnitTests.RateLimit -{ - public class ClientRateLimitMiddlewareTests - { - private readonly IRateLimitCounterHandler _rateLimitCounterHandler; - private readonly Mock _loggerFactory; - private readonly Mock _logger; - private readonly ClientRateLimitMiddleware _middleware; - private readonly RequestDelegate _next; - private DownstreamResponse _downstreamResponse; - private readonly string _url; - - public ClientRateLimitMiddlewareTests() - { - _url = "http://localhost:51879"; - var cacheEntryOptions = new MemoryCacheOptions(); - _rateLimitCounterHandler = new MemoryCacheRateLimitCounterHandler(new MemoryCache(cacheEntryOptions)); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _middleware = new ClientRateLimitMiddleware(_next, _loggerFactory.Object, _rateLimitCounterHandler); - } - - [Fact] - public void should_call_middleware_and_ratelimiting() - { - var upstreamTemplate = new UpstreamPathTemplateBuilder().Build(); - - var downstreamRoute = new DownstreamRouteBuilder() - .WithEnableRateLimiting(true) - .WithRateLimitOptions(new RateLimitOptions(true, "ClientId", () => new List(), false, string.Empty, string.Empty, new RateLimitRule("1s", 100, 3), 429)) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(upstreamTemplate) - .Build(); - - var route = new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - var downstreamRouteHolder = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); - - this.Given(x => x.WhenICallTheMiddlewareMultipleTimes(2, downstreamRouteHolder)) - .Then(x => x.ThenThereIsNoDownstreamResponse()) - .When(x => x.WhenICallTheMiddlewareMultipleTimes(3, downstreamRouteHolder)) - .Then(x => x.ThenTheResponseIs429()) - .BDDfy(); - } - - [Fact] - public void should_call_middleware_withWhitelistClient() - { - var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithEnableRateLimiting(true) - .WithRateLimitOptions( - new RateLimitOptions(true, "ClientId", () => new List { "ocelotclient2" }, false, string.Empty, string.Empty, new RateLimitRule("1s", 100, 3), 429)) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); - - this.Given(x => x.WhenICallTheMiddlewareWithWhiteClient(downstreamRoute)) - .Then(x => x.ThenThereIsNoDownstreamResponse()) - .BDDfy(); - } - - private void WhenICallTheMiddlewareMultipleTimes(int times, Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) - { - var httpContexts = new List(); - - for (var i = 0; i < times; i++) - { - var httpContext = new DefaultHttpContext - { - Response = - { - Body = new FakeStream(), - }, - }; - httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); - httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); - httpContext.Items.UpsertDownstreamRoute(downstreamRoute); - var clientId = "ocelotclient1"; - var request = new HttpRequestMessage(new HttpMethod("GET"), _url); - httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); - httpContext.Request.Headers.TryAdd("ClientId", clientId); - httpContexts.Add(httpContext); - } - - foreach (var httpContext in httpContexts) - { - _middleware.Invoke(httpContext).GetAwaiter().GetResult(); - var ds = httpContext.Items.DownstreamResponse(); - _downstreamResponse = ds; - } - } - - private void WhenICallTheMiddlewareWithWhiteClient(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) - { - var clientId = "ocelotclient2"; - - for (var i = 0; i < 10; i++) - { - var httpContext = new DefaultHttpContext - { - Response = - { - Body = new FakeStream(), - }, - }; - httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); - httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); - httpContext.Items.UpsertDownstreamRoute(downstreamRoute); - var request = new HttpRequestMessage(new HttpMethod("GET"), _url); - request.Headers.Add("ClientId", clientId); - httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); - httpContext.Request.Headers.TryAdd("ClientId", clientId); - _middleware.Invoke(httpContext).GetAwaiter().GetResult(); - var ds = httpContext.Items.DownstreamResponse(); - _downstreamResponse = ds; - } - } - - private void ThenTheResponseIs429() - { - var code = (int)_downstreamResponse.StatusCode; - code.ShouldBe(429); - } - - private void ThenThereIsNoDownstreamResponse() - { - _downstreamResponse.ShouldBeNull(); - } - } - - internal class FakeStream : Stream - { - public override void Flush() - { - //do nothing - //throw new System.NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new System.NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new System.NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new System.NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - //do nothing - } - - public override bool CanRead { get; } - public override bool CanSeek { get; } - public override bool CanWrite => true; - public override long Length { get; } - public override long Position { get; set; } - } -} diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs new file mode 100644 index 000000000..29c3b0dee --- /dev/null +++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs @@ -0,0 +1,218 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.RateLimiting; +using Ocelot.RateLimiting.Middleware; +using Ocelot.Request.Middleware; +using System.Text; +using _DownstreamRouteHolder_ = Ocelot.DownstreamRouteFinder.DownstreamRouteHolder; +using _RateLimiting_ = Ocelot.RateLimiting.RateLimiting; + +namespace Ocelot.UnitTests.RateLimiting; + +public class RateLimitingMiddlewareTests : UnitTest +{ + private readonly IRateLimitStorage _storage; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly RateLimitingMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly IRateLimiting _rateLimiting; + private readonly List _downstreamResponses; + private readonly string _url; + + public RateLimitingMiddlewareTests() + { + _url = "http://localhost:51879"; + var cacheEntryOptions = new MemoryCacheOptions(); + _storage = new MemoryCacheRateLimitStorage(new MemoryCache(cacheEntryOptions)); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _rateLimiting = new _RateLimiting_(_storage); + _middleware = new RateLimitingMiddleware(_next, _loggerFactory.Object, _rateLimiting); + _downstreamResponses = new(); + } + + [Fact] + [Trait("Feat", "37")] + public async Task Should_call_middleware_and_ratelimiting() + { + // Arrange + const long limit = 3L; + var upstreamTemplate = new UpstreamPathTemplateBuilder() + .Build(); + var downstreamRoute = new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new( + enableRateLimiting: true, + clientIdHeader: "ClientId", + getClientWhitelist: () => new List(), + disableRateLimitHeaders: false, + quotaExceededMessage: "Exceeding!", + rateLimitCounterPrefix: string.Empty, + new RateLimitRule("1s", 100.0D, limit), + (int)HttpStatusCode.TooManyRequests)) + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(upstreamTemplate) + .Build(); + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build(); + var downstreamRouteHolder = new _DownstreamRouteHolder_(new(), route); + + // Act, Assert + await WhenICallTheMiddlewareMultipleTimes(limit, downstreamRouteHolder); + _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); + + // Act, Assert: the next request should fail + await WhenICallTheMiddlewareMultipleTimes(3, downstreamRouteHolder); + _downstreamResponses.ShouldNotBeNull(); + for (int i = 0; i < _downstreamResponses.Count; i++) + { + var response = _downstreamResponses[i].ShouldNotBeNull(); + response.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $"Downstream Response no is {i}"); + var body = await response.Content.ReadAsStringAsync(); + body.ShouldBe("Exceeding!"); + } + } + + [Fact] + [Trait("Feat", "37")] + public async Task Should_call_middleware_withWhitelistClient() + { + // Arrange + var route = new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new( + enableRateLimiting: true, + clientIdHeader: "ClientId", + getClientWhitelist: () => new List { "ocelotclient2" }, + disableRateLimitHeaders: false, + quotaExceededMessage: "Exceeding!", + rateLimitCounterPrefix: string.Empty, + new RateLimitRule("1s", 100.0D, 3), + (int)HttpStatusCode.TooManyRequests)) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build(); + var downstreamRoute = new _DownstreamRouteHolder_(new(), route); + + // Act + await WhenICallTheMiddlewareWithWhiteClient(downstreamRoute); + + // Assert + _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); + } + + [Fact] + [Trait("Bug", "1590")] + public async Task MiddlewareInvoke_PeriodTimespanValueIsGreaterThanPeriod_StatusNotEqualTo429() + { + // Arrange + const long limit = 100L; + var upstreamTemplate = new UpstreamPathTemplateBuilder() + .Build(); + var downstreamRoute = new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new( + enableRateLimiting: true, + clientIdHeader: "ClientId", + getClientWhitelist: () => new List(), + disableRateLimitHeaders: false, + quotaExceededMessage: "Exceeding!", + rateLimitCounterPrefix: string.Empty, + new RateLimitRule("1s", 30.0D, limit), // bug scenario + (int)HttpStatusCode.TooManyRequests)) + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(upstreamTemplate) + .Build(); + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build(); + var downstreamRouteHolder = new _DownstreamRouteHolder_(new(), route); + + // Act, Assert: 100 requests must be successful + var contexts = await WhenICallTheMiddlewareMultipleTimes(limit, downstreamRouteHolder); // make 100 requests, but not exceed the limit + _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); + contexts.ForEach(ctx => + { + ctx.ShouldNotBeNull(); + ctx.Items.Errors().ShouldNotBeNull().ShouldBeEmpty(); // no errors + ctx.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK); // not 429 aka TooManyRequests + }); + + // Act, Assert: the next 101st request should fail + contexts = await WhenICallTheMiddlewareMultipleTimes(1, downstreamRouteHolder); + _downstreamResponses.ShouldNotBeNull(); + var ds = _downstreamResponses.SingleOrDefault().ShouldNotBeNull(); + ds.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $"Downstream Response no {limit + 1}"); + var body = await ds.Content.ReadAsStringAsync(); + body.ShouldBe("Exceeding!"); + contexts[0].Items.Errors().ShouldNotBeNull().ShouldNotBeEmpty(); // having errors + contexts[0].Items.Errors().Single().HttpStatusCode.ShouldBe((int)HttpStatusCode.TooManyRequests); + } + + private async Task> WhenICallTheMiddlewareMultipleTimes(long times, _DownstreamRouteHolder_ downstreamRoute) + { + var contexts = new List(); + _downstreamResponses.Clear(); + for (var i = 0; i < times; i++) + { + var context = new DefaultHttpContext(); + var stream = GetFakeStream($"{i}"); + context.Response.Body = stream; + context.Response.RegisterForDispose(stream); + context.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + context.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + context.Items.UpsertDownstreamRoute(downstreamRoute); + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + context.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); + context.Request.Headers.TryAdd("ClientId", "ocelotclient1"); + contexts.Add(context); + + await _middleware.Invoke(context); + + _downstreamResponses.Add(context.Items.DownstreamResponse()); + } + + return contexts; + } + + private static Stream GetFakeStream(string str) + { + byte[] data = Encoding.ASCII.GetBytes(str); + return new MemoryStream(data, 0, data.Length); + } + + private async Task WhenICallTheMiddlewareWithWhiteClient(_DownstreamRouteHolder_ downstreamRoute) + { + const string ClientId = "ocelotclient2"; + for (var i = 0; i < 10; i++) + { + var context = new DefaultHttpContext(); + var stream = GetFakeStream($"{i}"); + context.Response.Body = stream; + context.Response.RegisterForDispose(stream); + context.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + context.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + context.Items.UpsertDownstreamRoute(downstreamRoute); + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + request.Headers.Add("ClientId", ClientId); + context.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); + context.Request.Headers.TryAdd("ClientId", ClientId); + + await _middleware.Invoke(context); + + _downstreamResponses.Add(context.Items.DownstreamResponse()); + } + } +} diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs new file mode 100644 index 000000000..a4eb4738e --- /dev/null +++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs @@ -0,0 +1,268 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.RateLimiting; +using System.Runtime.CompilerServices; +using _RateLimiting_ = Ocelot.RateLimiting.RateLimiting; + +namespace Ocelot.UnitTests.RateLimiting; + +public sealed class RateLimitingTests +{ + private readonly Mock _storage; + private readonly _RateLimiting_ _sut; + + public RateLimitingTests() + { + _storage = new(); + _sut = new(_storage.Object); + } + + [Theory] + [Trait("Feat", "37")] + [InlineData(null)] + [InlineData("")] + public void ToTimespan_EmptyValue_ShouldReturnZero(string empty) + { + // Arrange, Act + var actual = _sut.ToTimespan(empty); + + // Assert + Assert.Equal(TimeSpan.Zero, actual); + } + + [Theory] + [Trait("Feat", "37")] + [InlineData("1a")] + [InlineData("2unknown")] + public void ToTimespan_UnknownType_ShouldThrowFormatException(string timespan) + { + // Arrange, Act, Assert + Assert.Throws( + () => _sut.ToTimespan(timespan)); + } + + [Theory] + [Trait("Feat", "37")] + [InlineData("1s", 1 * TimeSpan.TicksPerSecond)] + [InlineData("2m", 2 * TimeSpan.TicksPerMinute)] + [InlineData("3h", 3 * TimeSpan.TicksPerHour)] + [InlineData("4d", 4 * TimeSpan.TicksPerDay)] + public void ToTimespan_KnownType_HappyPath(string timespan, long ticks) + { + // Arrange + var expected = TimeSpan.FromTicks(ticks); + + // Act + var actual = _sut.ToTimespan(timespan); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + [Trait("PR", "1592")] + public void Count_NoEntry_StartCounting() + { + // Arrange + RateLimitCounter? arg1 = null; // No Entry + RateLimitRule arg2 = null; + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(1L, actual.TotalRequests); + Assert.True(DateTime.UtcNow - actual.StartedAt < TimeSpan.FromSeconds(1.0D)); + } + + [Fact] + [Trait("PR", "1592")] + public void Count_EntryHasNotExpired_IncrementedRequestCount() + { + // Arrange + long total = 2; + RateLimitCounter? arg1 = new RateLimitCounter(DateTime.UtcNow, null, total); // entry has not expired + RateLimitRule arg2 = new("1s", 1.0D, total + 1); // with not exceeding limit + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(total + 1, actual.TotalRequests); // incremented request count + Assert.Equal(arg1.Value.StartedAt, actual.StartedAt); // starting point has not changed + } + + [Fact] + [Trait("PR", "1592")] + public void Count_EntryHasNotExpiredAndExceedingLimit_IncrementedRequestCountWithRenewedStartMoment() + { + // Arrange + long total = 2; + RateLimitCounter? arg1 = new RateLimitCounter(DateTime.UtcNow, null, total); // entry has not expired + RateLimitRule arg2 = new("1s", 1.0D, 1L); + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(total + 1, actual.TotalRequests); // incremented request count + Assert.InRange(actual.StartedAt, arg1.Value.StartedAt, DateTime.UtcNow); // starting point has renewed and it is between StartedAt and Now + } + + [Fact] + [Trait("PR", "1592")] + public void Count_RateLimitExceeded_StartedCounting() + { + // Arrange + long total = 3, limit = total - 1; + TimeSpan periodTimespan = TimeSpan.FromSeconds(1.0D); + DateTime startedAt = DateTime.UtcNow.AddSeconds(-2.0), // 2 secs ago + exceededAt = startedAt + periodTimespan; // 1 second ago + RateLimitCounter? arg1 = new RateLimitCounter(startedAt, exceededAt, total); // Entry has expired + RateLimitRule arg2 = new("1s", periodTimespan.TotalSeconds, limit); // rate limit exceeded + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(1L, actual.TotalRequests); // started counting, the counter was changed + Assert.InRange(actual.StartedAt, arg1.Value.ExceededAt.Value, DateTime.UtcNow); // starting point has renewed and it is between exceededAt and Now + } + + [Fact] + [Trait("PR", "1592")] + public void Count_RateLimitNotExceededAndPeriodIsElapsed_StartedCountingByDefault() + { + // Arrange + long total = 3, limit = 3; + RateLimitCounter? arg1 = new RateLimitCounter(DateTime.UtcNow.AddSeconds(-2.0), null, total); // Entry has expired + RateLimitRule arg2 = new("1s", 1.0D, limit); // Rate limit not exceeded + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(1L, actual.TotalRequests); // started counting + Assert.True(DateTime.UtcNow - actual.StartedAt < TimeSpan.FromSeconds(1.0D)); // started now + } + + [Fact] + [Trait("PR", "1592")] + public void ProcessRequest_RateLimitExceededAndBanPeriodElapsed_StartedCounting() + { + // Arrange + const double periodTimespan = 2.0D; + const int millisecondsBeforeAfterEnding = 100; // current processing time of unit test should not take more 100 ms + DateTime now = DateTime.UtcNow, + startedAt = now.AddSeconds(-3).AddMilliseconds(millisecondsBeforeAfterEnding); + DateTime? exceededAt = null; + long totalRequests = 2L; + TimeSpan expiration = TimeSpan.Zero; + + var (identity, options) = SetupProcessRequest("3s", periodTimespan, totalRequests, + () => new RateLimitCounter(startedAt, exceededAt, totalRequests), + (value) => expiration = value); + + // Act 1 + var counter = _sut.ProcessRequest(identity, options); + + // Assert 1 + Assert.Equal(3L, counter.TotalRequests); // old counting -> 3 + Assert.Equal(startedAt, counter.StartedAt); // starting point was not changed + Assert.NotNull(counter.ExceededAt); // exceeded + Assert.Equal(DateTime.UtcNow.Second, counter.ExceededAt.Value.Second); // exceeded now, in the same second + + // Arrange 2 + TimeSpan shift = TimeSpan.FromSeconds(periodTimespan); // don't wait, just move to future + startedAt = counter.StartedAt - shift; // move to past + exceededAt = counter.ExceededAt - shift; // move to past + totalRequests = counter.TotalRequests; // 3 + + // Act 2 + var actual = _sut.ProcessRequest(identity, options); + + // Assert + Assert.Equal(1L, actual.TotalRequests); // started counting + Assert.InRange(actual.StartedAt, now, DateTime.UtcNow); // starting point has renewed and it is between test starting and Now + Assert.Null(actual.ExceededAt); + _storage.Verify(x => x.Remove(It.IsAny()), + Times.Never()); // Once()? Seems Remove is never called because of renewing + _storage.Verify(x => x.Get(It.IsAny()), + Times.Exactly(2)); + _storage.Verify(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(2)); + Assert.Equal(TimeSpan.FromSeconds(3), expiration); + } + + private (ClientRequestIdentity Identity, RateLimitOptions Options) SetupProcessRequest(string period, double periodTimespan, long limit, + Func counterFactory, Action expirationAction, [CallerMemberName] string testName = "") + { + ClientRequestIdentity identity = new(nameof(RateLimitingTests), "/" + testName, HttpMethods.Get); + RateLimitOptions options = new RateLimitOptionsBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitCounterPrefix(nameof(_RateLimiting_.ProcessRequest)) + .WithRateLimitRule(new RateLimitRule(period, periodTimespan, limit)) + .Build(); + _storage.Setup(x => x.Get(It.IsAny())) + .Returns(counterFactory); // counter value factory + _storage.Setup(x => x.Remove(It.IsAny())) + .Verifiable(); + expirationAction?.Invoke(TimeSpan.Zero); + _storage.Setup(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((id, counter, expirationTime) => expirationAction?.Invoke(expirationTime)) + .Verifiable(); + return (identity, options); + } + + [Fact] + [Trait("Bug", "1590")] + public void ProcessRequest_PeriodTimespanValueIsGreaterThanPeriod_ExpectedBehaviorAndExpirationInPeriod() + { + // Arrange: user scenario + const string period = "1s"; + const double periodTimespan = 30.0D; // seconds + const long limit = 100L, requestsPerSecond = 20L; + + // Arrange: setup + DateTime? startedAt = null; + TimeSpan expiration = TimeSpan.Zero; + long total = 1L, count = requestsPerSecond; + RateLimitCounter? current = null; + var (identity, options) = SetupProcessRequest(period, periodTimespan, limit, + () => current, + (value) => expiration = value); + + // Arrange 20 requests per period (1 sec) + var periodSeconds = TimeSpan.FromSeconds(double.Parse(period[0].ToString())); + var periodMilliseconds = periodSeconds.TotalMilliseconds; + int delay = (int)((periodMilliseconds - 200) / requestsPerSecond); // 20 requests per 1 second + + while (count > 0L) + { + // Act + var actual = _sut.ProcessRequest(identity, options); + + // life hack for the 1st request + if (count == requestsPerSecond) + { + startedAt = actual.StartedAt; // for the 1st request get expected value + } + + // Assert + Assert.True(actual.TotalRequests < limit); + actual.TotalRequests.ShouldBe(total++, $"Count is {count}"); + Assert.Equal(startedAt, actual.StartedAt); // starting point is not changed + Assert.Null(actual.ExceededAt); // no exceeding at all + Assert.Equal(periodSeconds, expiration); // expiration in the period + + // Arrange: next micro test + current = actual; + Thread.Sleep(delay); + count--; + } + + Assert.NotEqual(TimeSpan.FromSeconds(periodTimespan), expiration); // Not ban period expiration + Assert.Equal(periodSeconds, expiration); // last 20th request was in counting period + } +} diff --git a/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs index 33b797025..ba6e2c9a3 100644 --- a/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Repository { - public class ScopedRequestDataRepositoryTests + public class ScopedRequestDataRepositoryTests : UnitTest { private readonly IRequestScopedDataRepository _requestScopedDataRepository; private readonly IHttpContextAccessor _httpContextAccesor; diff --git a/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs index e61811314..7426b77aa 100644 --- a/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs +++ b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Request.Creator { - public class DownstreamRequestCreatorTests + public class DownstreamRequestCreatorTests : UnitTest { private readonly Mock _framework; private readonly DownstreamRequestCreator _downstreamRequestCreator; diff --git a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs index 854b70e6d..ed19d1aae 100644 --- a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Request; -public class DownstreamRequestInitialiserMiddlewareTests +public class DownstreamRequestInitialiserMiddlewareTests : UnitTest { private readonly DownstreamRequestInitialiserMiddleware _middleware; private readonly HttpContext _httpContext; diff --git a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs index 624dc779e..6f06d0295 100644 --- a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs +++ b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Request.Mapper; -public class RequestMapperTests +public class RequestMapperTests : UnitTest { private readonly HttpRequest _inputRequest; private readonly RequestMapper _requestMapper; @@ -206,8 +206,9 @@ public void Should_handle_no_content_length() [Fact] public void Should_map_content_headers() - { - var md5Bytes = MD5.HashData("some md5"u8.ToArray()); + { + var bytes = Encoding.UTF8.GetBytes("some md5"); + var md5Bytes = MD5.HashData(bytes); this.Given(_ => GivenTheInputRequestHasContent("This is my content")) .And(_ => GivenTheContentTypeIs("application/json")) diff --git a/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs b/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs index b8ae8a50f..105bb85de 100644 --- a/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs +++ b/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs @@ -35,8 +35,9 @@ public async Task Copy_body_to_stream_with_unknown_length_and_stream_content_sho var bytes = Encoding.UTF8.GetBytes(PayLoad); using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); - await CopyAsyncTest(new StreamHttpContent(_httpContext), - [inputStream, outputStream, StreamHttpContent.UnknownLength, false, CancellationToken.None]); + await CopyAsyncTest( + new StreamHttpContent(_httpContext), + new object[] { inputStream, outputStream, StreamHttpContent.UnknownLength, false, CancellationToken.None }); inputStream.Position = 0; outputStream.Position = 0; var result = Encoding.UTF8.GetString(outputStream.ToArray()); @@ -49,8 +50,9 @@ public async Task Copy_body_to_stream_with_body_length_and_stream_content_should var bytes = Encoding.UTF8.GetBytes(PayLoad); using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); - await CopyAsyncTest(new StreamHttpContent(_httpContext), - [inputStream, outputStream, bytes.Length, false, CancellationToken.None]); + await CopyAsyncTest( + new StreamHttpContent(_httpContext), + new object[] { inputStream, outputStream, bytes.Length, false, CancellationToken.None }); inputStream.Position = 0; outputStream.Position = 0; var result = Encoding.UTF8.GetString(outputStream.ToArray()); @@ -64,8 +66,9 @@ public async Task Should_throw_if_passed_body_length_does_not_match_real_body_le using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); await Assert.ThrowsAsync(async () => - await CopyAsyncTest(new StreamHttpContent(_httpContext), - [inputStream, outputStream, 10, false, CancellationToken.None])); + await CopyAsyncTest( + new StreamHttpContent(_httpContext), + new object[] { inputStream, outputStream, 10, false, CancellationToken.None })); } private StreamHttpContent StreamHttpContentFactory() diff --git a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs index 641f9c9cc..bac14d857 100644 --- a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.RequestId { - public class RequestIdMiddlewareTests + public class RequestIdMiddlewareTests : UnitTest { private readonly HttpRequestMessage _downstreamRequest; private string _value; diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs index a6c74c115..dde971d69 100644 --- a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Requester { - public class DelegatingHandlerHandlerProviderFactoryTests + public class DelegatingHandlerHandlerProviderFactoryTests : UnitTest { private DelegatingHandlerHandlerFactory _factory; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index 717ca04ad..9060494b6 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Requester { - public class HttpRequesterMiddlewareTests + public class HttpRequesterMiddlewareTests : UnitTest { private readonly Mock _requester; private Response _response; diff --git a/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs b/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs index fd4eb7d4d..57e3b803a 100644 --- a/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs +++ b/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs @@ -14,7 +14,7 @@ namespace Ocelot.UnitTests.Requester; [Trait("PR", "1824")] -public class MessageInvokerPoolTests +public class MessageInvokerPoolTests : UnitTest { private DownstreamRoute _downstreamRoute1; private DownstreamRoute _downstreamRoute2; @@ -321,7 +321,7 @@ private Mock GetHandlerFactory() { var handlerFactory = new Mock(); handlerFactory.Setup(x => x.Get(It.IsAny())) - .Returns(new OkResponse>>([])); + .Returns(new OkResponse>>(new())); return handlerFactory; } @@ -333,7 +333,7 @@ private DownstreamRoute DownstreamRouteFactory(string path) .WithLoadBalancerKey(string.Empty) .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, false, 10, TimeSpan.FromSeconds(120))) - .WithUpstreamHttpMethod(["Get"]) + .WithUpstreamHttpMethod(new() { "Get" }) .Build(); return downstreamRoute; diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index e80551117..da55e1752 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Responder { - public class ErrorsToHttpStatusCodeMapperTests + public class ErrorsToHttpStatusCodeMapperTests : UnitTest { private readonly IErrorsToHttpStatusCodeMapper _codeMapper; private int _result; @@ -86,7 +86,7 @@ public void should_return_not_found(OcelotErrorCode errorCode) [Fact] public void should_return_request_entity_too_large() { - ShouldMapErrorsToStatusCode([OcelotErrorCode.PayloadTooLargeError], HttpStatusCode.RequestEntityTooLarge); + ShouldMapErrorsToStatusCode(new() { OcelotErrorCode.PayloadTooLargeError }, HttpStatusCode.RequestEntityTooLarge); } [Fact] diff --git a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs index bde95def8..5a8bc5644 100644 --- a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Responder { - public class ResponderMiddlewareTests + public class ResponderMiddlewareTests : UnitTest { private readonly Mock _responder; private readonly Mock _codeMapper; diff --git a/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs b/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs index bdc86842d..f32d46bb1 100644 --- a/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs +++ b/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Security { - public class IPSecurityPolicyTests + public class IPSecurityPolicyTests : UnitTest { private readonly DownstreamRouteBuilder _downstreamRouteBuilder; private readonly IPSecurityPolicy _ipSecurityPolicy; diff --git a/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs b/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs index e8310bb92..c24ccffc2 100644 --- a/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Security { - public class SecurityMiddlewareTests + public class SecurityMiddlewareTests : UnitTest { private readonly List> _securityPolicyList; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs index 1de3f5cea..ad1f7bb72 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery { - public class ConfigurationServiceProviderTests + public class ConfigurationServiceProviderTests : UnitTest { private ConfigurationServiceProvider _serviceProvider; private List _result; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs index 88d33f89c..8f416b2d4 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery { - public class ServiceDiscoveryProviderFactoryTests + public class ServiceDiscoveryProviderFactoryTests : UnitTest { private ServiceProviderConfiguration _serviceConfig; private Response _result; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs index 0836afffc..1032cbac5 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery { - public class ServiceFabricServiceDiscoveryProviderTests + public class ServiceFabricServiceDiscoveryProviderTests : UnitTest { private ServiceFabricServiceDiscoveryProvider _provider; private ServiceFabricConfiguration _config; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs index 22e6dc316..33670a0dd 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs @@ -3,7 +3,7 @@ // nothing in use namespace Ocelot.UnitTests.ServiceDiscovery { - public class ServiceRegistryTests + public class ServiceRegistryTests : UnitTest { private Service _service; private List _services; diff --git a/test/Ocelot.UnitTests/UnitTest.cs b/test/Ocelot.UnitTests/UnitTest.cs index aa7b716b3..a50782f5a 100644 --- a/test/Ocelot.UnitTests/UnitTest.cs +++ b/test/Ocelot.UnitTests/UnitTest.cs @@ -1,7 +1,14 @@ -namespace Ocelot.UnitTests; +using TestStack.BDDfy.Configuration; + +namespace Ocelot.UnitTests; public class UnitTest { + public UnitTest() + { + Configurator.Processors.ConsoleReport.Disable(); + } + protected readonly Guid _testId = Guid.NewGuid(); protected string TestID { get => _testId.ToString("N"); } diff --git a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs index d5190ce01..42af9716d 100644 --- a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.WebSockets; -public class WebSocketsProxyMiddlewareTests +public class WebSocketsProxyMiddlewareTests : UnitTest { private readonly WebSocketsProxyMiddleware _middleware;