From 7680ef87b4a9f5a4eb38f1bd2243e6c926d5750d Mon Sep 17 00:00:00 2001 From: Twan van Buuren Date: Sun, 2 Apr 2023 23:32:13 +0200 Subject: [PATCH 1/4] feature 862 Add intial spanner module as startingpoint --- .gitignore | 3 + Testcontainers.sln | 16 +- src/Testcontainers.Spanner/.editorconfig | 241 ++++++++++++++++++ .../HttpResponseMessageExtensions.cs | 24 ++ .../RestApi/v1/SpannerOperationDto.cs | 10 + .../RestApi/v1/SpannerStatusDto.cs | 8 + src/Testcontainers.Spanner/SpannerBuilder.cs | 126 +++++++++ .../SpannerConfiguration.cs | 67 +++++ .../SpannerContainer.cs | 153 +++++++++++ .../Testcontainers.Spanner.csproj | 14 + src/Testcontainers.Spanner/Usings copy.cs | 8 + src/Testcontainers.Spanner/Usings.cs | 10 + .../.editorconfig | 241 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 1 + .../SpannerContainerExecuteDdlAsyncTests.cs | 30 +++ .../SpannerContainerStartAsyncTests.cs | 100 ++++++++ .../Testcontainers.Spanner.Tests.csproj | 36 +++ tests/Testcontainers.Spanner.Tests/Usings.cs | 1 + 18 files changed, 1088 insertions(+), 1 deletion(-) create mode 100644 src/Testcontainers.Spanner/.editorconfig create mode 100644 src/Testcontainers.Spanner/HttpResponseMessageExtensions.cs create mode 100644 src/Testcontainers.Spanner/RestApi/v1/SpannerOperationDto.cs create mode 100644 src/Testcontainers.Spanner/RestApi/v1/SpannerStatusDto.cs create mode 100644 src/Testcontainers.Spanner/SpannerBuilder.cs create mode 100644 src/Testcontainers.Spanner/SpannerConfiguration.cs create mode 100644 src/Testcontainers.Spanner/SpannerContainer.cs create mode 100644 src/Testcontainers.Spanner/Testcontainers.Spanner.csproj create mode 100644 src/Testcontainers.Spanner/Usings copy.cs create mode 100644 src/Testcontainers.Spanner/Usings.cs create mode 100644 tests/Testcontainers.Spanner.Tests/.editorconfig create mode 100644 tests/Testcontainers.Spanner.Tests/Properties/AssemblyInfo.cs create mode 100644 tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs create mode 100644 tests/Testcontainers.Spanner.Tests/SpannerContainerStartAsyncTests.cs create mode 100644 tests/Testcontainers.Spanner.Tests/Testcontainers.Spanner.Tests.csproj create mode 100644 tests/Testcontainers.Spanner.Tests/Usings.cs diff --git a/.gitignore b/.gitignore index a68c08dfa..d7e2ad777 100644 --- a/.gitignore +++ b/.gitignore @@ -357,3 +357,6 @@ test-coverage/ # SonarQube .sonarqube/ + +# Visual Studio Code +.vscode/ diff --git a/Testcontainers.sln b/Testcontainers.sln index a86fd5cb6..2e506756a 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 @@ -121,6 +121,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Spanner", "src\Testcontainers.Spanner\Testcontainers.Spanner.csproj", "{7026E057-9F15-4947-AB8C-4AF034323A3E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Spanner.Tests", "tests\Testcontainers.Spanner.Tests\Testcontainers.Spanner.Tests.csproj", "{A84920E9-D781-4457-BD32-43B32C2D451B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -346,6 +350,14 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU + {7026E057-9F15-4947-AB8C-4AF034323A3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7026E057-9F15-4947-AB8C-4AF034323A3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7026E057-9F15-4947-AB8C-4AF034323A3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7026E057-9F15-4947-AB8C-4AF034323A3E}.Release|Any CPU.Build.0 = Release|Any CPU + {A84920E9-D781-4457-BD32-43B32C2D451B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A84920E9-D781-4457-BD32-43B32C2D451B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A84920E9-D781-4457-BD32-43B32C2D451B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A84920E9-D781-4457-BD32-43B32C2D451B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -402,5 +414,7 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {7026E057-9F15-4947-AB8C-4AF034323A3E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {A84920E9-D781-4457-BD32-43B32C2D451B} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Spanner/.editorconfig b/src/Testcontainers.Spanner/.editorconfig new file mode 100644 index 000000000..52ebb0dd2 --- /dev/null +++ b/src/Testcontainers.Spanner/.editorconfig @@ -0,0 +1,241 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +tab_width = 2 +trim_trailing_whitespace = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +tab_width = 2 +trim_trailing_whitespace = true + +#### .NET Coding Conventions #### + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning + +# Expression-level preferences +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = false:suggestion +csharp_style_throw_expression = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = true:warning + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = false:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = true: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 = false:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = false:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +#### 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 + +# IDE0063: Use simple 'using' statement +csharp_prefer_simple_using_statement = false:suggestion +csharp_using_directive_placement = outside_namespace:suggestion +csharp_style_namespace_declarations = file_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning +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 = warning +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 = warning +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.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 + +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 + + +# Define the 'private_fields' symbol group: +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +# Define the 'private_static_fields' symbol group +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +# Define the 'underscored' naming style +dotnet_naming_style.underscored.capitalization = camel_case +dotnet_naming_style.underscored.required_prefix = _ + +# Define the 'private_fields_underscored' naming rule +dotnet_naming_rule.private_fields_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_underscored.style = underscored +dotnet_naming_rule.private_fields_underscored.severity = error + +# Define the 'private_static_fields_none' naming rule +dotnet_naming_rule.private_static_fields_none.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_none.style = underscored +dotnet_naming_rule.private_static_fields_none.severity = none +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion diff --git a/src/Testcontainers.Spanner/HttpResponseMessageExtensions.cs b/src/Testcontainers.Spanner/HttpResponseMessageExtensions.cs new file mode 100644 index 000000000..dfc2a3cb4 --- /dev/null +++ b/src/Testcontainers.Spanner/HttpResponseMessageExtensions.cs @@ -0,0 +1,24 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Testcontainers.Spanner; +internal static class HttpResponseMessageExtensions +{ + internal static async Task ValidateAsync(this Task response, string operation, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + var result = await response; + + if (!result.IsSuccessStatusCode) + { + string resultContent = await result.Content.ReadAsStringAsync(); + string message = + $"Failed to {operation} with status code {result.StatusCode} response: {resultContent}"; + throw new InvalidOperationException(message); + } + + return result; + } +} diff --git a/src/Testcontainers.Spanner/RestApi/v1/SpannerOperationDto.cs b/src/Testcontainers.Spanner/RestApi/v1/SpannerOperationDto.cs new file mode 100644 index 000000000..ba88fa2b1 --- /dev/null +++ b/src/Testcontainers.Spanner/RestApi/v1/SpannerOperationDto.cs @@ -0,0 +1,10 @@ +namespace Testcontainers.Spanner.RestApi.v1; + +public class SpannerOperationDto +{ + public string Name { get; set; } = string.Empty; + public bool Done { get; set; } + public SpannerStatusDto? Error { get; set; } + public object? Response { get; set; } + public object? MetaData { get; set; } +} diff --git a/src/Testcontainers.Spanner/RestApi/v1/SpannerStatusDto.cs b/src/Testcontainers.Spanner/RestApi/v1/SpannerStatusDto.cs new file mode 100644 index 000000000..d0da206b1 --- /dev/null +++ b/src/Testcontainers.Spanner/RestApi/v1/SpannerStatusDto.cs @@ -0,0 +1,8 @@ +namespace Testcontainers.Spanner.RestApi.v1; + +public class SpannerStatusDto +{ + public int Code { get; set; } + public string? Message { get; set; } + public object[]? Details { get; set; } +} diff --git a/src/Testcontainers.Spanner/SpannerBuilder.cs b/src/Testcontainers.Spanner/SpannerBuilder.cs new file mode 100644 index 000000000..9ff67ba9e --- /dev/null +++ b/src/Testcontainers.Spanner/SpannerBuilder.cs @@ -0,0 +1,126 @@ +namespace Testcontainers.Spanner; + +/// +[PublicAPI] +public sealed class SpannerBuilder : ContainerBuilder +{ + private const string DefaultProjectId = "my-project"; + private const string DefaultInstanceId = "my-instance"; + private const string DefaultDatabaseId = "my-database"; + private const string SpannerEmulatorImage = "gcr.io/cloud-spanner-emulator/emulator"; + + + /// + /// Initializes a new instance of the class. + /// + public SpannerBuilder() + : this(new SpannerConfiguration()) + { + + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private SpannerBuilder(SpannerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + // /// + protected override SpannerConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the DatabaseId. + /// + /// The DatabaseId. + /// A configured instance of . + public SpannerBuilder WithDatabaseId(string databaseId) + => Merge(DockerResourceConfiguration, new SpannerConfiguration(databaseId: databaseId)); + + /// + /// Sets the InstanceId. + /// + /// The InstanceId. + /// A configured instance of . + public SpannerBuilder WithInstanceId(string instanceId) + => Merge(DockerResourceConfiguration, new SpannerConfiguration(instanceId: instanceId)); + + /// + /// Sets the ProjectId. + /// + /// The ProjectId. + /// A configured instance of . + public SpannerBuilder WithProjectId(string projectId) + => Merge(DockerResourceConfiguration, new SpannerConfiguration(projectId: projectId)); + + + /// + public override SpannerContainer Build() + { + Validate(); + return new SpannerContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + + /// + protected override SpannerBuilder Init() + { + return base.Init() + .WithImage(SpannerEmulatorImage) + .WithPortBinding(SpannerContainer.InternalGrpcPort, true) + .WithPortBinding(SpannerContainer.InternalRestPort, true) + .WithProjectId(DefaultProjectId) + .WithInstanceId(DefaultInstanceId) + .WithDatabaseId(DefaultDatabaseId) + .WithWaitStrategy( + Wait + .ForUnixContainer() + // The default wait for port implementation keeps waiting untill the test times out, therefor now using this custom flavor of the same concept + .UntilMessageIsLogged($".+REST server listening at 0.0.0.0:{SpannerContainer.InternalRestPort}") + .UntilMessageIsLogged($".+gRPC server listening at 0.0.0.0:{SpannerContainer.InternalGrpcPort}") + ); + + } + + + /// + protected override void Validate() + { + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.ProjectId, nameof(DockerResourceConfiguration.ProjectId)) + .NotNull() + .NotEmpty(); + + _ = Guard.Argument(DockerResourceConfiguration.InstanceId, nameof(DockerResourceConfiguration.InstanceId)) + .NotNull() + .NotEmpty(); + + _ = Guard.Argument(DockerResourceConfiguration.DatabaseId, nameof(DockerResourceConfiguration.DatabaseId)) + .NotNull() + .NotEmpty(); + + } + + /// + protected override SpannerBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SpannerConfiguration(resourceConfiguration)); + } + + /// + protected override SpannerBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SpannerConfiguration(resourceConfiguration)); + } + + /// + protected override SpannerBuilder Merge(SpannerConfiguration oldValue, SpannerConfiguration newValue) + { + return new SpannerBuilder(new SpannerConfiguration(oldValue, newValue)); + } +} diff --git a/src/Testcontainers.Spanner/SpannerConfiguration.cs b/src/Testcontainers.Spanner/SpannerConfiguration.cs new file mode 100644 index 000000000..9c47a164c --- /dev/null +++ b/src/Testcontainers.Spanner/SpannerConfiguration.cs @@ -0,0 +1,67 @@ +namespace Testcontainers.Spanner; + +/// +[PublicAPI] +public sealed class SpannerConfiguration : ContainerConfiguration +{ + public string? ProjectId { get; } + public string? InstanceId { get; } + public string? DatabaseId { get; } + + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public SpannerConfiguration(string? projectId = null, string? instanceId = null, string? databaseId = null) + { + ProjectId = projectId; + InstanceId = instanceId; + DatabaseId = databaseId; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public SpannerConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public SpannerConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public SpannerConfiguration(SpannerConfiguration resourceConfiguration) + : this(new SpannerConfiguration(), resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public SpannerConfiguration(SpannerConfiguration oldValue, SpannerConfiguration newValue) + : base(oldValue, newValue) + { + ProjectId = BuildConfiguration.Combine(oldValue.ProjectId, newValue.ProjectId); + InstanceId = BuildConfiguration.Combine(oldValue.InstanceId, newValue.InstanceId); + DatabaseId = BuildConfiguration.Combine(oldValue.DatabaseId, newValue.DatabaseId); + } +} diff --git a/src/Testcontainers.Spanner/SpannerContainer.cs b/src/Testcontainers.Spanner/SpannerContainer.cs new file mode 100644 index 000000000..c3dbda3e2 --- /dev/null +++ b/src/Testcontainers.Spanner/SpannerContainer.cs @@ -0,0 +1,153 @@ +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Testcontainers.Spanner.RestApi.v1; + +namespace Testcontainers.Spanner; + +/// +[PublicAPI] +public sealed class SpannerContainer : DockerContainer, IDisposable +{ + internal const int InternalGrpcPort = 9010; + internal const int InternalRestPort = 9020; + private const string EnvironmentVariableEmulatorHost = "SPANNER_EMULATOR_HOST"; + private readonly SpannerConfiguration _configuration; + private readonly HttpClient _webClient = new(); + + public int GrpcPort => GetMappedPublicPort(InternalGrpcPort); + public int RestPort => GetMappedPublicPort(InternalRestPort); + + public string ConnectionString + { + get + { + return new StringBuilder("Data Source=projects/") + .Append(_configuration.ProjectId) + .Append("/instances/") + .Append(_configuration.InstanceId) + .Append("/databases/") + .Append(_configuration.DatabaseId) + .Append("; EmulatorDetection=EmulatorOnly; Host=") + .Append(Hostname) + .Append("; Port=") + .Append(GrpcPort) + .ToString(); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public SpannerContainer(SpannerConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + _configuration = configuration; + } + + public override async Task StartAsync(CancellationToken ct = default) + { + await base.StartAsync(ct); + + Environment.SetEnvironmentVariable(EnvironmentVariableEmulatorHost, $"{Hostname}:{GrpcPort}"); +#pragma warning disable S5332 + // warning S5332: Using http protocol is insecure. Use https instead. + // This doesn't apply here, as it is a testing setup only localhost connection encryption is not required. + _webClient.BaseAddress = new Uri($"http://{Hostname}:{RestPort}"); +#pragma warning restore S5332 + + await CreateSpannerInstance(ct); + await CreateDatabase(ct); + } + + private async Task CreateSpannerInstance(CancellationToken ct = default) + { + var resource = new + { + instanceId = _configuration.InstanceId, + instance = new + { + config = "emulator-config", + displayName = _configuration.InstanceId, + } + }; + string requestUri = $"v1/projects/{_configuration.ProjectId}/instances"; + string resourceDescription = "instance"; + + await CreateResource(requestUri, resource, resourceDescription, ct); + } + + private async Task CreateDatabase(CancellationToken ct = default) + { + var resource = new + { + createStatement = $"CREATE DATABASE `{_configuration.DatabaseId}`", + }; + + + string requestUri = $"v1/projects/{_configuration.ProjectId}/instances/{_configuration.InstanceId}/databases"; + string resourceDescription = "database"; + + await CreateResource(requestUri, resource, resourceDescription, ct); + } + + public async Task ExecuteDdlAsync(params string[] ddl) + { + string requestUri = $"v1/projects/{_configuration.ProjectId}/instances/{_configuration.InstanceId}/databases/{_configuration.DatabaseId}/ddl"; + var request = new { statements = ddl }; + + var response = await _webClient.PatchAsJsonAsync(requestUri, request) + .ValidateAsync("update ddl"); + + var operation = await response.Content.ReadFromJsonAsync() ?? + throw new InvalidOperationException( + "Executing ddl resulted in a response with a body deserializing as SpannerOperation to null"); + + await AwaitOperationAsync(operation); + + if (operation.Error != null) + { + throw new InvalidOperationException($"Failed to execute ddl operation with error {operation.Error}"); + } + } + + private async Task AwaitOperationAsync(SpannerOperationDto operation) + { + int delayInSeconds = 1; + const int maxDelayInSeconds = 10; + + while (!operation.Done) + { + await Task.Delay(TimeSpan.FromSeconds(delayInSeconds)); + string operationName = operation.Name; + var operationResponse = await _webClient.GetAsync($"v1/{operationName}").ValidateAsync("get operation"); + + operation = await operationResponse.Content.ReadFromJsonAsync() ?? + throw new InvalidOperationException( + $"Getting operation resulted in null deserialization of body for operation name {operationName}"); + if (delayInSeconds * 2 < maxDelayInSeconds) + { + delayInSeconds *= 2; + } + } + } + + + private async Task CreateResource(string requestUri, object resource, string resourceDescription, CancellationToken ct = default) + { + await _webClient.PostAsJsonAsync(requestUri, resource, cancellationToken: ct) + .ValidateAsync($"create {resourceDescription}", ct: ct); + } + + public void Dispose() => _webClient.Dispose(); + + protected override async ValueTask DisposeAsyncCore() + { + _webClient.Dispose(); + await base.DisposeAsyncCore(); + } +} diff --git a/src/Testcontainers.Spanner/Testcontainers.Spanner.csproj b/src/Testcontainers.Spanner/Testcontainers.Spanner.csproj new file mode 100644 index 000000000..2520c0060 --- /dev/null +++ b/src/Testcontainers.Spanner/Testcontainers.Spanner.csproj @@ -0,0 +1,14 @@ + + + netstandard2.0;netstandard2.1 + latest + enable + + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Spanner/Usings copy.cs b/src/Testcontainers.Spanner/Usings copy.cs new file mode 100644 index 000000000..0192278ae --- /dev/null +++ b/src/Testcontainers.Spanner/Usings copy.cs @@ -0,0 +1,8 @@ +global using System; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; diff --git a/src/Testcontainers.Spanner/Usings.cs b/src/Testcontainers.Spanner/Usings.cs new file mode 100644 index 000000000..f889bad0a --- /dev/null +++ b/src/Testcontainers.Spanner/Usings.cs @@ -0,0 +1,10 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/tests/Testcontainers.Spanner.Tests/.editorconfig b/tests/Testcontainers.Spanner.Tests/.editorconfig new file mode 100644 index 000000000..52ebb0dd2 --- /dev/null +++ b/tests/Testcontainers.Spanner.Tests/.editorconfig @@ -0,0 +1,241 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +tab_width = 2 +trim_trailing_whitespace = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +tab_width = 2 +trim_trailing_whitespace = true + +#### .NET Coding Conventions #### + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning + +# Expression-level preferences +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = false:suggestion +csharp_style_throw_expression = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = true:warning + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = false:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = true: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 = false:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = false:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +#### 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 + +# IDE0063: Use simple 'using' statement +csharp_prefer_simple_using_statement = false:suggestion +csharp_using_directive_placement = outside_namespace:suggestion +csharp_style_namespace_declarations = file_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning +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 = warning +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 = warning +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.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 + +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 + + +# Define the 'private_fields' symbol group: +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +# Define the 'private_static_fields' symbol group +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +# Define the 'underscored' naming style +dotnet_naming_style.underscored.capitalization = camel_case +dotnet_naming_style.underscored.required_prefix = _ + +# Define the 'private_fields_underscored' naming rule +dotnet_naming_rule.private_fields_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_underscored.style = underscored +dotnet_naming_rule.private_fields_underscored.severity = error + +# Define the 'private_static_fields_none' naming rule +dotnet_naming_rule.private_static_fields_none.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_none.style = underscored +dotnet_naming_rule.private_static_fields_none.severity = none +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion diff --git a/tests/Testcontainers.Spanner.Tests/Properties/AssemblyInfo.cs b/tests/Testcontainers.Spanner.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..e5cc5d402 --- /dev/null +++ b/tests/Testcontainers.Spanner.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs b/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs new file mode 100644 index 000000000..5cbc4a7c2 --- /dev/null +++ b/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs @@ -0,0 +1,30 @@ +using Google.Cloud.Spanner.Data; + +namespace Testcontainers.Spanner.Tests; + +public class SpannerContainerExecuteDdlAsyncTests +{ + + + [Fact] + public async Task GivenContainerIsStartedWhenDdlExecutedThenInsertSucceeds() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + await containerManager.StartAsync(); + + // Act + await containerManager.ExecuteDdlAsync("CREATE TABLE MyTable( MyTableId STRING(64) NOT NULL, Name STRING(50) NOT NULL) PRIMARY KEY (MyTableId)"); + + // Assert + await using (var connection = new SpannerConnection(containerManager.ConnectionString)) + { + await connection.OpenAsync(); + var dml = connection.CreateDmlCommand("Insert into MyTable (MyTableId, Name ) Values ('a_Id', 'a_Name')"); + Assert.Equal(1, await dml.ExecuteNonQueryAsync()); + } + } + } +} diff --git a/tests/Testcontainers.Spanner.Tests/SpannerContainerStartAsyncTests.cs b/tests/Testcontainers.Spanner.Tests/SpannerContainerStartAsyncTests.cs new file mode 100644 index 000000000..f50cff78b --- /dev/null +++ b/tests/Testcontainers.Spanner.Tests/SpannerContainerStartAsyncTests.cs @@ -0,0 +1,100 @@ +using DotNet.Testcontainers.Containers; +using Google.Cloud.Spanner.Data; + +namespace Testcontainers.Spanner.Tests; + +public class SpannerContainerStartAsyncTests +{ + [Fact] + public async Task WhenCompleteThenContainerIsRunning() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + + // Act + await containerManager.StartAsync(); + // Assert + + Assert.Equal(TestcontainersStates.Running, containerManager.State); + } + } + + [Fact] + public async Task WhenCompleteThenContainerHasRandomizedRestPortMapped() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); + // Assert + + Assert.NotEqual(default, containerManager.RestPort); + Assert.NotEqual(9020, containerManager.RestPort); + } + } + + [Fact] + public async Task WhenCompleteThenContainerHasRandomizedGrpcPortMapped() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); + // Assert + + Assert.NotEqual(default, containerManager.GrpcPort); + Assert.NotEqual(9010, containerManager.GrpcPort); + } + } + + + [Fact] + public async Task WhenCompleteThenEnvironmentVariableEmulatorHostSet() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); + + // Assert + Assert.Equal($"{containerManager.Hostname}:{containerManager.GrpcPort}", Environment.GetEnvironmentVariable("SPANNER_EMULATOR_HOST")); + } + } + + [Fact] + public async Task WhenCompleteThenConnectionStringUseableToConnect() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); + + // Assert + using (var connection = new SpannerConnection(containerManager.ConnectionString)) + { + // Assert is not failing in executing, as status on connection will be ok, even if connectionstring is wrong + await connection.OpenAsync(); + var ddl = connection.CreateDdlCommand(@"CREATE TABLE MyTable +( + MyTableId STRING(64) NOT NULL, + Name STRING(50) NOT NULL +) +PRIMARY KEY (MyTableId)"); + ddl.CommandTimeout = 1; + _ = await ddl.ExecuteNonQueryAsync(); + // Supress git warning having no assert, the assertion is not failing up to here + Assert.True(true); + } + } + } +} diff --git a/tests/Testcontainers.Spanner.Tests/Testcontainers.Spanner.Tests.csproj b/tests/Testcontainers.Spanner.Tests/Testcontainers.Spanner.Tests.csproj new file mode 100644 index 000000000..13dd41184 --- /dev/null +++ b/tests/Testcontainers.Spanner.Tests/Testcontainers.Spanner.Tests.csproj @@ -0,0 +1,36 @@ + + + + net6.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/Testcontainers.Spanner.Tests/Usings.cs b/tests/Testcontainers.Spanner.Tests/Usings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/tests/Testcontainers.Spanner.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file From 51a5b00df8edd18d038b2abfde5ad57aae99a746 Mon Sep 17 00:00:00 2001 From: Twan van Buuren Date: Mon, 3 Apr 2023 23:08:14 +0200 Subject: [PATCH 2/4] feat: Test project enhanced --- src/Testcontainers.Spanner/Usings copy.cs | 8 ------- src/Testcontainers.Spanner/Usings.cs | 4 +--- .../SpannerContainerExecuteDdlAsyncTests.cs | 22 ++++++++++++++++++- .../Testcontainers.Spanner.Tests.csproj | 1 - tests/Testcontainers.Spanner.Tests/Usings.cs | 4 +++- 5 files changed, 25 insertions(+), 14 deletions(-) delete mode 100644 src/Testcontainers.Spanner/Usings copy.cs diff --git a/src/Testcontainers.Spanner/Usings copy.cs b/src/Testcontainers.Spanner/Usings copy.cs deleted file mode 100644 index 0192278ae..000000000 --- a/src/Testcontainers.Spanner/Usings copy.cs +++ /dev/null @@ -1,8 +0,0 @@ -global using System; -global using Docker.DotNet.Models; -global using DotNet.Testcontainers; -global using DotNet.Testcontainers.Builders; -global using DotNet.Testcontainers.Configurations; -global using DotNet.Testcontainers.Containers; -global using JetBrains.Annotations; -global using Microsoft.Extensions.Logging; diff --git a/src/Testcontainers.Spanner/Usings.cs b/src/Testcontainers.Spanner/Usings.cs index f889bad0a..0192278ae 100644 --- a/src/Testcontainers.Spanner/Usings.cs +++ b/src/Testcontainers.Spanner/Usings.cs @@ -1,10 +1,8 @@ global using System; -global using System.Collections.Generic; -global using System.Linq; global using Docker.DotNet.Models; global using DotNet.Testcontainers; global using DotNet.Testcontainers.Builders; global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; global using JetBrains.Annotations; -global using Microsoft.Extensions.Logging; \ No newline at end of file +global using Microsoft.Extensions.Logging; diff --git a/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs b/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs index 5cbc4a7c2..0c10dde38 100644 --- a/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs +++ b/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs @@ -4,6 +4,7 @@ namespace Testcontainers.Spanner.Tests; public class SpannerContainerExecuteDdlAsyncTests { + private const string ddl = "CREATE TABLE MyTable( MyTableId STRING(64) NOT NULL, Name STRING(50) NOT NULL) PRIMARY KEY (MyTableId)"; [Fact] @@ -16,7 +17,7 @@ public async Task GivenContainerIsStartedWhenDdlExecutedThenInsertSucceeds() await containerManager.StartAsync(); // Act - await containerManager.ExecuteDdlAsync("CREATE TABLE MyTable( MyTableId STRING(64) NOT NULL, Name STRING(50) NOT NULL) PRIMARY KEY (MyTableId)"); + await containerManager.ExecuteDdlAsync(ddl); // Assert await using (var connection = new SpannerConnection(containerManager.ConnectionString)) @@ -27,4 +28,23 @@ public async Task GivenContainerIsStartedWhenDdlExecutedThenInsertSucceeds() } } } + + [Fact] + public async Task GivenContainerIsNotStartedWhenDdlExecutedThenInValidOperationExceptionIsThrown() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + //await containerManager.StartAsync(); + + // Act // assert + await Assert.ThrowsAnyAsync(async () => + { + await containerManager.ExecuteDdlAsync(ddl); + }); + + + } + } } diff --git a/tests/Testcontainers.Spanner.Tests/Testcontainers.Spanner.Tests.csproj b/tests/Testcontainers.Spanner.Tests/Testcontainers.Spanner.Tests.csproj index 13dd41184..08b599aed 100644 --- a/tests/Testcontainers.Spanner.Tests/Testcontainers.Spanner.Tests.csproj +++ b/tests/Testcontainers.Spanner.Tests/Testcontainers.Spanner.Tests.csproj @@ -2,7 +2,6 @@ net6.0 - enable enable false diff --git a/tests/Testcontainers.Spanner.Tests/Usings.cs b/tests/Testcontainers.Spanner.Tests/Usings.cs index 8c927eb74..7bb6613f8 100644 --- a/tests/Testcontainers.Spanner.Tests/Usings.cs +++ b/tests/Testcontainers.Spanner.Tests/Usings.cs @@ -1 +1,3 @@ -global using Xunit; \ No newline at end of file +global using System; +global using System.Threading.Tasks; +global using Xunit; From 587fb9c433832c723e6d8377369a0b1718150ffd Mon Sep 17 00:00:00 2001 From: Twan van Buuren Date: Sat, 8 Apr 2023 18:59:15 +0200 Subject: [PATCH 3/4] feat: Process the simple review comments --- src/Testcontainers.Spanner/.editorconfig | 240 ------------------ src/Testcontainers.Spanner/SpannerBuilder.cs | 14 +- .../SpannerContainer.cs | 6 +- .../.editorconfig | 240 ------------------ 4 files changed, 11 insertions(+), 489 deletions(-) diff --git a/src/Testcontainers.Spanner/.editorconfig b/src/Testcontainers.Spanner/.editorconfig index 52ebb0dd2..78b36ca08 100644 --- a/src/Testcontainers.Spanner/.editorconfig +++ b/src/Testcontainers.Spanner/.editorconfig @@ -1,241 +1 @@ root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 2 -insert_final_newline = true -tab_width = 2 -trim_trailing_whitespace = true - -# C# files -[*.cs] - -#### Core EditorConfig Options #### - -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 2 -insert_final_newline = true -tab_width = 2 -trim_trailing_whitespace = true - -#### .NET Coding Conventions #### - -# this. and Me. preferences -dotnet_style_qualification_for_event = false:suggestion -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_property = false:suggestion - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning - -# Expression-level preferences -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_inlined_variable_declaration = false:suggestion -csharp_style_throw_expression = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_object_initializer = true:suggestion -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion - -# Field preferences -dotnet_style_readonly_field = true:warning - -# Parameter preferences -dotnet_code_quality_unused_parameters = all:suggestion - -#### C# Coding Conventions #### - -# var preferences -csharp_style_var_elsewhere = true:warning -csharp_style_var_for_built_in_types = false:warning -csharp_style_var_when_type_is_apparent = true:warning - -# Expression-bodied members -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_indexers = false:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_expression_bodied_methods = true: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 = false:suggestion -csharp_style_pattern_matching_over_is_with_cast_check = false:suggestion - -# Null-checking preferences -csharp_style_conditional_delegate_call = true:suggestion - -# Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async - -# Code-block preferences -csharp_prefer_braces = true:suggestion - -# Expression-level preferences -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_pattern_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:warning -csharp_style_unused_value_expression_statement_preference = discard_variable:silent - -#### 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 - -# IDE0063: Use simple 'using' statement -csharp_prefer_simple_using_statement = false:suggestion -csharp_using_directive_placement = outside_namespace:suggestion -csharp_style_namespace_declarations = file_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent - -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning -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 = warning -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 = warning -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.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 - -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 - - -# Define the 'private_fields' symbol group: -dotnet_naming_symbols.private_fields.applicable_kinds = field -dotnet_naming_symbols.private_fields.applicable_accessibilities = private - -# Define the 'private_static_fields' symbol group -dotnet_naming_symbols.private_static_fields.applicable_kinds = field -dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private -dotnet_naming_symbols.private_static_fields.required_modifiers = static - -# Define the 'underscored' naming style -dotnet_naming_style.underscored.capitalization = camel_case -dotnet_naming_style.underscored.required_prefix = _ - -# Define the 'private_fields_underscored' naming rule -dotnet_naming_rule.private_fields_underscored.symbols = private_fields -dotnet_naming_rule.private_fields_underscored.style = underscored -dotnet_naming_rule.private_fields_underscored.severity = error - -# Define the 'private_static_fields_none' naming rule -dotnet_naming_rule.private_static_fields_none.symbols = private_static_fields -dotnet_naming_rule.private_static_fields_none.style = underscored -dotnet_naming_rule.private_static_fields_none.severity = none -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_prefer_static_local_function = true:suggestion -csharp_style_prefer_readonly_struct = true:suggestion -csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent -csharp_style_prefer_pattern_matching = true:silent -csharp_style_prefer_switch_expression = true:suggestion -csharp_style_prefer_not_pattern = true:suggestion -csharp_style_prefer_extended_property_pattern = true:suggestion diff --git a/src/Testcontainers.Spanner/SpannerBuilder.cs b/src/Testcontainers.Spanner/SpannerBuilder.cs index 9ff67ba9e..f47225119 100644 --- a/src/Testcontainers.Spanner/SpannerBuilder.cs +++ b/src/Testcontainers.Spanner/SpannerBuilder.cs @@ -7,7 +7,11 @@ public sealed class SpannerBuilder : ContainerBuilder @@ -71,8 +75,8 @@ protected override SpannerBuilder Init() { return base.Init() .WithImage(SpannerEmulatorImage) - .WithPortBinding(SpannerContainer.InternalGrpcPort, true) - .WithPortBinding(SpannerContainer.InternalRestPort, true) + .WithPortBinding(InternalGrpcPort, true) + .WithPortBinding(InternalRestPort, true) .WithProjectId(DefaultProjectId) .WithInstanceId(DefaultInstanceId) .WithDatabaseId(DefaultDatabaseId) @@ -80,8 +84,8 @@ protected override SpannerBuilder Init() Wait .ForUnixContainer() // The default wait for port implementation keeps waiting untill the test times out, therefor now using this custom flavor of the same concept - .UntilMessageIsLogged($".+REST server listening at 0.0.0.0:{SpannerContainer.InternalRestPort}") - .UntilMessageIsLogged($".+gRPC server listening at 0.0.0.0:{SpannerContainer.InternalGrpcPort}") + .UntilMessageIsLogged($".+REST server listening at 0.0.0.0:{InternalRestPort}") + .UntilMessageIsLogged($".+gRPC server listening at 0.0.0.0:{InternalGrpcPort}") ); } diff --git a/src/Testcontainers.Spanner/SpannerContainer.cs b/src/Testcontainers.Spanner/SpannerContainer.cs index c3dbda3e2..42cbc6982 100644 --- a/src/Testcontainers.Spanner/SpannerContainer.cs +++ b/src/Testcontainers.Spanner/SpannerContainer.cs @@ -11,14 +11,12 @@ namespace Testcontainers.Spanner; [PublicAPI] public sealed class SpannerContainer : DockerContainer, IDisposable { - internal const int InternalGrpcPort = 9010; - internal const int InternalRestPort = 9020; private const string EnvironmentVariableEmulatorHost = "SPANNER_EMULATOR_HOST"; private readonly SpannerConfiguration _configuration; private readonly HttpClient _webClient = new(); - public int GrpcPort => GetMappedPublicPort(InternalGrpcPort); - public int RestPort => GetMappedPublicPort(InternalRestPort); + public int GrpcPort => GetMappedPublicPort(SpannerBuilder.InternalGrpcPort); + public int RestPort => GetMappedPublicPort(SpannerBuilder.InternalRestPort); public string ConnectionString { diff --git a/tests/Testcontainers.Spanner.Tests/.editorconfig b/tests/Testcontainers.Spanner.Tests/.editorconfig index 52ebb0dd2..78b36ca08 100644 --- a/tests/Testcontainers.Spanner.Tests/.editorconfig +++ b/tests/Testcontainers.Spanner.Tests/.editorconfig @@ -1,241 +1 @@ root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 2 -insert_final_newline = true -tab_width = 2 -trim_trailing_whitespace = true - -# C# files -[*.cs] - -#### Core EditorConfig Options #### - -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 2 -insert_final_newline = true -tab_width = 2 -trim_trailing_whitespace = true - -#### .NET Coding Conventions #### - -# this. and Me. preferences -dotnet_style_qualification_for_event = false:suggestion -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_property = false:suggestion - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning - -# Expression-level preferences -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_inlined_variable_declaration = false:suggestion -csharp_style_throw_expression = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_object_initializer = true:suggestion -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion - -# Field preferences -dotnet_style_readonly_field = true:warning - -# Parameter preferences -dotnet_code_quality_unused_parameters = all:suggestion - -#### C# Coding Conventions #### - -# var preferences -csharp_style_var_elsewhere = true:warning -csharp_style_var_for_built_in_types = false:warning -csharp_style_var_when_type_is_apparent = true:warning - -# Expression-bodied members -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_indexers = false:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_expression_bodied_methods = true: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 = false:suggestion -csharp_style_pattern_matching_over_is_with_cast_check = false:suggestion - -# Null-checking preferences -csharp_style_conditional_delegate_call = true:suggestion - -# Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async - -# Code-block preferences -csharp_prefer_braces = true:suggestion - -# Expression-level preferences -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_pattern_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:warning -csharp_style_unused_value_expression_statement_preference = discard_variable:silent - -#### 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 - -# IDE0063: Use simple 'using' statement -csharp_prefer_simple_using_statement = false:suggestion -csharp_using_directive_placement = outside_namespace:suggestion -csharp_style_namespace_declarations = file_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent - -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning -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 = warning -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 = warning -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.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 - -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 - - -# Define the 'private_fields' symbol group: -dotnet_naming_symbols.private_fields.applicable_kinds = field -dotnet_naming_symbols.private_fields.applicable_accessibilities = private - -# Define the 'private_static_fields' symbol group -dotnet_naming_symbols.private_static_fields.applicable_kinds = field -dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private -dotnet_naming_symbols.private_static_fields.required_modifiers = static - -# Define the 'underscored' naming style -dotnet_naming_style.underscored.capitalization = camel_case -dotnet_naming_style.underscored.required_prefix = _ - -# Define the 'private_fields_underscored' naming rule -dotnet_naming_rule.private_fields_underscored.symbols = private_fields -dotnet_naming_rule.private_fields_underscored.style = underscored -dotnet_naming_rule.private_fields_underscored.severity = error - -# Define the 'private_static_fields_none' naming rule -dotnet_naming_rule.private_static_fields_none.symbols = private_static_fields -dotnet_naming_rule.private_static_fields_none.style = underscored -dotnet_naming_rule.private_static_fields_none.severity = none -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_prefer_static_local_function = true:suggestion -csharp_style_prefer_readonly_struct = true:suggestion -csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent -csharp_style_prefer_pattern_matching = true:silent -csharp_style_prefer_switch_expression = true:suggestion -csharp_style_prefer_not_pattern = true:suggestion -csharp_style_prefer_extended_property_pattern = true:suggestion From 37e0c9225ef3cbec642d3991d711ae9f6ab4ed72 Mon Sep 17 00:00:00 2001 From: Twan van Buuren Date: Fri, 12 May 2023 00:44:01 +0200 Subject: [PATCH 4/4] Process further review comments and add demo for the necessity of disable parallelisation because of the environment variable usage --- .../RestApi/v1/SpannerOperationDto.cs | 10 -- .../RestApi/v1/SpannerStatusDto.cs | 8 - src/Testcontainers.Spanner/SpannerBuilder.cs | 29 ---- .../SpannerConfiguration.cs | 13 +- .../SpannerContainer.cs | 123 +------------ .../Testcontainers.Spanner.csproj | 7 +- .../Properties/AssemblyInfo.cs | 2 +- .../SpannerContainerExecuteDdlAsyncTests.cs | 50 ------ .../SpannerContainerStartAsyncTests.cs | 163 ++++++++++-------- ...nerContainerTestsForEnvironmentVariable.cs | 97 +++++++++++ 10 files changed, 194 insertions(+), 308 deletions(-) delete mode 100644 src/Testcontainers.Spanner/RestApi/v1/SpannerOperationDto.cs delete mode 100644 src/Testcontainers.Spanner/RestApi/v1/SpannerStatusDto.cs delete mode 100644 tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs create mode 100644 tests/Testcontainers.Spanner.Tests/SpannerContainerTestsForEnvironmentVariable.cs diff --git a/src/Testcontainers.Spanner/RestApi/v1/SpannerOperationDto.cs b/src/Testcontainers.Spanner/RestApi/v1/SpannerOperationDto.cs deleted file mode 100644 index ba88fa2b1..000000000 --- a/src/Testcontainers.Spanner/RestApi/v1/SpannerOperationDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Testcontainers.Spanner.RestApi.v1; - -public class SpannerOperationDto -{ - public string Name { get; set; } = string.Empty; - public bool Done { get; set; } - public SpannerStatusDto? Error { get; set; } - public object? Response { get; set; } - public object? MetaData { get; set; } -} diff --git a/src/Testcontainers.Spanner/RestApi/v1/SpannerStatusDto.cs b/src/Testcontainers.Spanner/RestApi/v1/SpannerStatusDto.cs deleted file mode 100644 index d0da206b1..000000000 --- a/src/Testcontainers.Spanner/RestApi/v1/SpannerStatusDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Testcontainers.Spanner.RestApi.v1; - -public class SpannerStatusDto -{ - public int Code { get; set; } - public string? Message { get; set; } - public object[]? Details { get; set; } -} diff --git a/src/Testcontainers.Spanner/SpannerBuilder.cs b/src/Testcontainers.Spanner/SpannerBuilder.cs index f47225119..75dc15f1d 100644 --- a/src/Testcontainers.Spanner/SpannerBuilder.cs +++ b/src/Testcontainers.Spanner/SpannerBuilder.cs @@ -5,8 +5,6 @@ namespace Testcontainers.Spanner; public sealed class SpannerBuilder : ContainerBuilder { private const string DefaultProjectId = "my-project"; - private const string DefaultInstanceId = "my-instance"; - private const string DefaultDatabaseId = "my-database"; private const string SpannerEmulatorImage = "gcr.io/cloud-spanner-emulator/emulator:1.5.3"; @@ -37,22 +35,6 @@ private SpannerBuilder(SpannerConfiguration resourceConfiguration) // /// protected override SpannerConfiguration DockerResourceConfiguration { get; } - /// - /// Sets the DatabaseId. - /// - /// The DatabaseId. - /// A configured instance of . - public SpannerBuilder WithDatabaseId(string databaseId) - => Merge(DockerResourceConfiguration, new SpannerConfiguration(databaseId: databaseId)); - - /// - /// Sets the InstanceId. - /// - /// The InstanceId. - /// A configured instance of . - public SpannerBuilder WithInstanceId(string instanceId) - => Merge(DockerResourceConfiguration, new SpannerConfiguration(instanceId: instanceId)); - /// /// Sets the ProjectId. /// @@ -78,8 +60,6 @@ protected override SpannerBuilder Init() .WithPortBinding(InternalGrpcPort, true) .WithPortBinding(InternalRestPort, true) .WithProjectId(DefaultProjectId) - .WithInstanceId(DefaultInstanceId) - .WithDatabaseId(DefaultDatabaseId) .WithWaitStrategy( Wait .ForUnixContainer() @@ -99,15 +79,6 @@ protected override void Validate() _ = Guard.Argument(DockerResourceConfiguration.ProjectId, nameof(DockerResourceConfiguration.ProjectId)) .NotNull() .NotEmpty(); - - _ = Guard.Argument(DockerResourceConfiguration.InstanceId, nameof(DockerResourceConfiguration.InstanceId)) - .NotNull() - .NotEmpty(); - - _ = Guard.Argument(DockerResourceConfiguration.DatabaseId, nameof(DockerResourceConfiguration.DatabaseId)) - .NotNull() - .NotEmpty(); - } /// diff --git a/src/Testcontainers.Spanner/SpannerConfiguration.cs b/src/Testcontainers.Spanner/SpannerConfiguration.cs index 9c47a164c..041de36e6 100644 --- a/src/Testcontainers.Spanner/SpannerConfiguration.cs +++ b/src/Testcontainers.Spanner/SpannerConfiguration.cs @@ -2,24 +2,17 @@ namespace Testcontainers.Spanner; /// [PublicAPI] -public sealed class SpannerConfiguration : ContainerConfiguration +public class SpannerConfiguration : ContainerConfiguration { public string? ProjectId { get; } - public string? InstanceId { get; } - public string? DatabaseId { get; } - /// /// Initializes a new instance of the class. /// /// - /// - /// - public SpannerConfiguration(string? projectId = null, string? instanceId = null, string? databaseId = null) + public SpannerConfiguration(string? projectId = null) { ProjectId = projectId; - InstanceId = instanceId; - DatabaseId = databaseId; } /// @@ -61,7 +54,5 @@ public SpannerConfiguration(SpannerConfiguration oldValue, SpannerConfiguration : base(oldValue, newValue) { ProjectId = BuildConfiguration.Combine(oldValue.ProjectId, newValue.ProjectId); - InstanceId = BuildConfiguration.Combine(oldValue.InstanceId, newValue.InstanceId); - DatabaseId = BuildConfiguration.Combine(oldValue.DatabaseId, newValue.DatabaseId); } } diff --git a/src/Testcontainers.Spanner/SpannerContainer.cs b/src/Testcontainers.Spanner/SpannerContainer.cs index 42cbc6982..d062e8ca1 100644 --- a/src/Testcontainers.Spanner/SpannerContainer.cs +++ b/src/Testcontainers.Spanner/SpannerContainer.cs @@ -1,48 +1,26 @@ -using System.Net.Http; -using System.Net.Http.Json; using System.Text; using System.Threading; using System.Threading.Tasks; -using Testcontainers.Spanner.RestApi.v1; namespace Testcontainers.Spanner; /// [PublicAPI] -public sealed class SpannerContainer : DockerContainer, IDisposable +public sealed class SpannerContainer : DockerContainer { private const string EnvironmentVariableEmulatorHost = "SPANNER_EMULATOR_HOST"; private readonly SpannerConfiguration _configuration; - private readonly HttpClient _webClient = new(); public int GrpcPort => GetMappedPublicPort(SpannerBuilder.InternalGrpcPort); public int RestPort => GetMappedPublicPort(SpannerBuilder.InternalRestPort); - public string ConnectionString - { - get - { - return new StringBuilder("Data Source=projects/") - .Append(_configuration.ProjectId) - .Append("/instances/") - .Append(_configuration.InstanceId) - .Append("/databases/") - .Append(_configuration.DatabaseId) - .Append("; EmulatorDetection=EmulatorOnly; Host=") - .Append(Hostname) - .Append("; Port=") - .Append(GrpcPort) - .ToString(); - } - } - /// /// Initializes a new instance of the class. /// /// The container configuration. /// The logger. public SpannerContainer(SpannerConfiguration configuration, ILogger logger) - : base(configuration, logger) + : base(configuration, logger) { _configuration = configuration; } @@ -52,100 +30,5 @@ public override async Task StartAsync(CancellationToken ct = default) await base.StartAsync(ct); Environment.SetEnvironmentVariable(EnvironmentVariableEmulatorHost, $"{Hostname}:{GrpcPort}"); -#pragma warning disable S5332 - // warning S5332: Using http protocol is insecure. Use https instead. - // This doesn't apply here, as it is a testing setup only localhost connection encryption is not required. - _webClient.BaseAddress = new Uri($"http://{Hostname}:{RestPort}"); -#pragma warning restore S5332 - - await CreateSpannerInstance(ct); - await CreateDatabase(ct); - } - - private async Task CreateSpannerInstance(CancellationToken ct = default) - { - var resource = new - { - instanceId = _configuration.InstanceId, - instance = new - { - config = "emulator-config", - displayName = _configuration.InstanceId, - } - }; - string requestUri = $"v1/projects/{_configuration.ProjectId}/instances"; - string resourceDescription = "instance"; - - await CreateResource(requestUri, resource, resourceDescription, ct); - } - - private async Task CreateDatabase(CancellationToken ct = default) - { - var resource = new - { - createStatement = $"CREATE DATABASE `{_configuration.DatabaseId}`", - }; - - - string requestUri = $"v1/projects/{_configuration.ProjectId}/instances/{_configuration.InstanceId}/databases"; - string resourceDescription = "database"; - - await CreateResource(requestUri, resource, resourceDescription, ct); - } - - public async Task ExecuteDdlAsync(params string[] ddl) - { - string requestUri = $"v1/projects/{_configuration.ProjectId}/instances/{_configuration.InstanceId}/databases/{_configuration.DatabaseId}/ddl"; - var request = new { statements = ddl }; - - var response = await _webClient.PatchAsJsonAsync(requestUri, request) - .ValidateAsync("update ddl"); - - var operation = await response.Content.ReadFromJsonAsync() ?? - throw new InvalidOperationException( - "Executing ddl resulted in a response with a body deserializing as SpannerOperation to null"); - - await AwaitOperationAsync(operation); - - if (operation.Error != null) - { - throw new InvalidOperationException($"Failed to execute ddl operation with error {operation.Error}"); - } - } - - private async Task AwaitOperationAsync(SpannerOperationDto operation) - { - int delayInSeconds = 1; - const int maxDelayInSeconds = 10; - - while (!operation.Done) - { - await Task.Delay(TimeSpan.FromSeconds(delayInSeconds)); - string operationName = operation.Name; - var operationResponse = await _webClient.GetAsync($"v1/{operationName}").ValidateAsync("get operation"); - - operation = await operationResponse.Content.ReadFromJsonAsync() ?? - throw new InvalidOperationException( - $"Getting operation resulted in null deserialization of body for operation name {operationName}"); - if (delayInSeconds * 2 < maxDelayInSeconds) - { - delayInSeconds *= 2; - } - } - } - - - private async Task CreateResource(string requestUri, object resource, string resourceDescription, CancellationToken ct = default) - { - await _webClient.PostAsJsonAsync(requestUri, resource, cancellationToken: ct) - .ValidateAsync($"create {resourceDescription}", ct: ct); - } - - public void Dispose() => _webClient.Dispose(); - - protected override async ValueTask DisposeAsyncCore() - { - _webClient.Dispose(); - await base.DisposeAsyncCore(); } -} +} \ No newline at end of file diff --git a/src/Testcontainers.Spanner/Testcontainers.Spanner.csproj b/src/Testcontainers.Spanner/Testcontainers.Spanner.csproj index 2520c0060..1d8a7a69e 100644 --- a/src/Testcontainers.Spanner/Testcontainers.Spanner.csproj +++ b/src/Testcontainers.Spanner/Testcontainers.Spanner.csproj @@ -5,10 +5,11 @@ enable - - + + + - + \ No newline at end of file diff --git a/tests/Testcontainers.Spanner.Tests/Properties/AssemblyInfo.cs b/tests/Testcontainers.Spanner.Tests/Properties/AssemblyInfo.cs index e5cc5d402..7a23a8555 100644 --- a/tests/Testcontainers.Spanner.Tests/Properties/AssemblyInfo.cs +++ b/tests/Testcontainers.Spanner.Tests/Properties/AssemblyInfo.cs @@ -1 +1 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] +[assembly: CollectionBehavior(DisableTestParallelization = true)] // The c.net client libraries alwys use the host from the environment variable, even if another one is passed diff --git a/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs b/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs deleted file mode 100644 index 0c10dde38..000000000 --- a/tests/Testcontainers.Spanner.Tests/SpannerContainerExecuteDdlAsyncTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Google.Cloud.Spanner.Data; - -namespace Testcontainers.Spanner.Tests; - -public class SpannerContainerExecuteDdlAsyncTests -{ - private const string ddl = "CREATE TABLE MyTable( MyTableId STRING(64) NOT NULL, Name STRING(50) NOT NULL) PRIMARY KEY (MyTableId)"; - - - [Fact] - public async Task GivenContainerIsStartedWhenDdlExecutedThenInsertSucceeds() - { - // Arrange - var builder = new SpannerBuilder(); - await using (var containerManager = builder.Build()) - { - await containerManager.StartAsync(); - - // Act - await containerManager.ExecuteDdlAsync(ddl); - - // Assert - await using (var connection = new SpannerConnection(containerManager.ConnectionString)) - { - await connection.OpenAsync(); - var dml = connection.CreateDmlCommand("Insert into MyTable (MyTableId, Name ) Values ('a_Id', 'a_Name')"); - Assert.Equal(1, await dml.ExecuteNonQueryAsync()); - } - } - } - - [Fact] - public async Task GivenContainerIsNotStartedWhenDdlExecutedThenInValidOperationExceptionIsThrown() - { - // Arrange - var builder = new SpannerBuilder(); - await using (var containerManager = builder.Build()) - { - //await containerManager.StartAsync(); - - // Act // assert - await Assert.ThrowsAnyAsync(async () => - { - await containerManager.ExecuteDdlAsync(ddl); - }); - - - } - } -} diff --git a/tests/Testcontainers.Spanner.Tests/SpannerContainerStartAsyncTests.cs b/tests/Testcontainers.Spanner.Tests/SpannerContainerStartAsyncTests.cs index f50cff78b..c363a71eb 100644 --- a/tests/Testcontainers.Spanner.Tests/SpannerContainerStartAsyncTests.cs +++ b/tests/Testcontainers.Spanner.Tests/SpannerContainerStartAsyncTests.cs @@ -1,100 +1,111 @@ using DotNet.Testcontainers.Containers; -using Google.Cloud.Spanner.Data; +using Google.Api.Gax; +using Google.Cloud.Spanner.Admin.Instance.V1; + namespace Testcontainers.Spanner.Tests; public class SpannerContainerStartAsyncTests { - [Fact] - public async Task WhenCompleteThenContainerIsRunning() - { - // Arrange - var builder = new SpannerBuilder(); - await using (var containerManager = builder.Build()) + private const string InstanceId = "my-pretty-test-instance"; + private const string ProjectUri = "projects/my-project"; + + [Fact] + public async Task WhenCompleteThenContainerIsRunning() { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { - // Act - await containerManager.StartAsync(); - // Assert + // Act + await containerManager.StartAsync(); + // Assert - Assert.Equal(TestcontainersStates.Running, containerManager.State); + Assert.Equal(TestcontainersStates.Running, containerManager.State); + } } - } - - [Fact] - public async Task WhenCompleteThenContainerHasRandomizedRestPortMapped() - { - // Arrange - var builder = new SpannerBuilder(); - await using (var containerManager = builder.Build()) + + [Fact] + public async Task WhenCompleteThenContainerHasRandomizedRestPortMapped() { - // Act - await containerManager.StartAsync(); - // Assert + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); + // Assert - Assert.NotEqual(default, containerManager.RestPort); - Assert.NotEqual(9020, containerManager.RestPort); + Assert.NotEqual(default, containerManager.RestPort); + Assert.NotEqual(9020, containerManager.RestPort); + } } - } - - [Fact] - public async Task WhenCompleteThenContainerHasRandomizedGrpcPortMapped() - { - // Arrange - var builder = new SpannerBuilder(); - await using (var containerManager = builder.Build()) + + [Fact] + public async Task WhenCompleteThenContainerHasRandomizedGrpcPortMapped() { - // Act - await containerManager.StartAsync(); - // Assert + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); - Assert.NotEqual(default, containerManager.GrpcPort); - Assert.NotEqual(9010, containerManager.GrpcPort); + // Assert + Assert.NotEqual(default, containerManager.GrpcPort); + Assert.NotEqual(9010, containerManager.GrpcPort); + } } - } - [Fact] - public async Task WhenCompleteThenEnvironmentVariableEmulatorHostSet() - { - // Arrange - var builder = new SpannerBuilder(); - await using (var containerManager = builder.Build()) + [Fact] + public async Task WhenCompleteThenEnvironmentVariableEmulatorHostSet() { - // Act - await containerManager.StartAsync(); + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); - // Assert - Assert.Equal($"{containerManager.Hostname}:{containerManager.GrpcPort}", Environment.GetEnvironmentVariable("SPANNER_EMULATOR_HOST")); + // Assert + Assert.Equal($"{containerManager.Hostname}:{containerManager.GrpcPort}", Environment.GetEnvironmentVariable("SPANNER_EMULATOR_HOST")); + } } - } - - [Fact] - public async Task WhenCompleteThenConnectionStringUseableToConnect() - { - // Arrange - var builder = new SpannerBuilder(); - await using (var containerManager = builder.Build()) + + [Fact] + public async Task WhenCompleteThenConnectionIsUseable() { - // Act - await containerManager.StartAsync(); - - // Assert - using (var connection = new SpannerConnection(containerManager.ConnectionString)) - { - // Assert is not failing in executing, as status on connection will be ok, even if connectionstring is wrong - await connection.OpenAsync(); - var ddl = connection.CreateDdlCommand(@"CREATE TABLE MyTable -( - MyTableId STRING(64) NOT NULL, - Name STRING(50) NOT NULL -) -PRIMARY KEY (MyTableId)"); - ddl.CommandTimeout = 1; - _ = await ddl.ExecuteNonQueryAsync(); - // Supress git warning having no assert, the assertion is not failing up to here - Assert.True(true); - } + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); + + var clientBuilder = new InstanceAdminClientBuilder() + { + EmulatorDetection = EmulatorDetection.EmulatorOrProduction, + Endpoint = $"{containerManager.Hostname}:{containerManager.GrpcPort}", + }; + + var client = await clientBuilder.BuildAsync(); + + // Act + await client.CreateInstanceAsync(new CreateInstanceRequest() + { + InstanceId = InstanceId, + Parent = ProjectUri, + }); + + var instances = client.ListInstances(new ListInstancesRequest + { + Parent = ProjectUri, + }); + + // Assert + Assert.Contains(instances, i => i.InstanceName.InstanceId == InstanceId); + } } - } } diff --git a/tests/Testcontainers.Spanner.Tests/SpannerContainerTestsForEnvironmentVariable.cs b/tests/Testcontainers.Spanner.Tests/SpannerContainerTestsForEnvironmentVariable.cs new file mode 100644 index 000000000..5814bfe11 --- /dev/null +++ b/tests/Testcontainers.Spanner.Tests/SpannerContainerTestsForEnvironmentVariable.cs @@ -0,0 +1,97 @@ +using Google.Api.Gax; +using Google.Cloud.Spanner.Admin.Instance.V1; + + +namespace Testcontainers.Spanner.Tests; + +public class SpannerContainerTestsForEnvironmentVariable +{ + private const string InstanceId = "my-pretty-test-instance"; + private const string ProjectUri = "projects/my-project"; + + + + [Fact] + public async Task WhenProductionOnlyConnectionNotUsable() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); + + var clientBuilder = new InstanceAdminClientBuilder() + { + EmulatorDetection = EmulatorDetection.EmulatorOrProduction, + Endpoint = $"{containerManager.Hostname}:{containerManager.GrpcPort}", + }; + + var client = await clientBuilder.BuildAsync(); + + // Assert + await Assert.ThrowsAsync(async () => await client.CreateInstanceAsync(new CreateInstanceRequest() + { + InstanceId = InstanceId, + Parent = ProjectUri, + })); + } + } + + [Fact] + public async Task WhenNotSetThenConnectionIsNotUseable() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); + + var clientBuilder = new InstanceAdminClientBuilder() + { + EmulatorDetection = EmulatorDetection.EmulatorOrProduction, + Endpoint = $"{containerManager.Hostname}:{containerManager.GrpcPort}", + }; + + var client = await clientBuilder.BuildAsync(); + + Environment.SetEnvironmentVariable("SPANNER_EMULATOR_HOST", null); + + // Assert + await Assert.ThrowsAsync(async () => await client.CreateInstanceAsync(new CreateInstanceRequest() + { + InstanceId = InstanceId, + Parent = ProjectUri, + })); + } + } + [Fact] + public async Task WhenNotSetAndEmulatorOnlyThenConnectionIsNotUseable() + { + // Arrange + var builder = new SpannerBuilder(); + await using (var containerManager = builder.Build()) + { + // Act + await containerManager.StartAsync(); + + var clientBuilder = new InstanceAdminClientBuilder() + { + EmulatorDetection = EmulatorDetection.EmulatorOnly, + Endpoint = $"{containerManager.Hostname}:{containerManager.GrpcPort}", + }; + + var client = await clientBuilder.BuildAsync(); + + Environment.SetEnvironmentVariable("SPANNER_EMULATOR_HOST", null); + + // Assert + await Assert.ThrowsAsync(async () => await client.CreateInstanceAsync(new CreateInstanceRequest() + { + InstanceId = InstanceId, + Parent = ProjectUri, + })); + } + } +}