diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0e84cbd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,455 @@ +# Copyright 2025-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 50 +ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = normal +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_enum_constants_wrap = off +ij_java_enum_field_annotation_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_generate_use_type_annotation_before_type = true +ij_java_if_brace_force = never +ij_java_imports_layout = *,|,javax.**,java.**,|,$* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 30 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = false +ij_java_new_line_when_body_is_presented = false +ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_java_parameter_annotation_wrap = off +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = false +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_inside_block_braces_when_body_is_present = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = +ij_java_subclass_name_suffix = Impl +ij_java_switch_expressions_wrap = normal +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_prefix = +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false +ij_java_wrap_semicolon_after_call_chain = false + +[*.properties] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.kt,*.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = normal +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_indent_before_arrow_on_new_line = true +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..6407dae --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +# .git-blame-ignore-revs +# Spotless initial cleanup +a711643b057a1f07f5590cad30d123b7ccadf1b0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f91f646 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/.gitignore b/.gitignore index 524f096..58a3df9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,37 @@ -# Compiled class file -*.class +# Copyright 2025-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -# Log file -*.log +*~ -# BlueJ files -*.ctxt +# Mac +.DS_Store -# Mobile Tools for Java (J2ME) -.mtj.tmp/ +# Eclipse +/.classpath +/.project +/.settings +/bin/ -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar +# Intellij IDEA +/.idea/ +/*.ipr +/*.iws +/*.iml +/out/ +!/.idea/inspectionProfiles/Project_Default.xml -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* +# Gradle +.gradle/ +.kotlin/ +build/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b521482 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +## Contributing to the MongoDB Spring Session extension + +Thank you for your interest in contributing to the MongoDB Spring Session extension. + +We are building this software together and strongly encourage contributions from the community that are within the guidelines set forth +below. + +Bug Fixes and New Features +-------------------------- + +Before starting to write code, look for existing [tickets](https://jira.mongodb.org/browse/JAVAF) or +[create one](https://jira.mongodb.org/secure/CreateIssue!default.jspa) for your bug, issue, or feature request. This helps the community +avoid working on something that might not be of interest or which has already been addressed. + +Pull Requests +------------- + +Pull requests should generally be made against the main (default) branch and include relevant tests, if applicable. + +Code should compile with the Java 17 compiler and tests should pass under all Java versions which the driver currently +supports. + +The results of pull request testing will be appended to the request. If any tests do not pass, or relevant tests are not included, the +pull request will not be considered. + +To run all checks locally run: + +```console +./gradlew clean check +``` + +Talk To Us +---------- + +If you want to work on something or have questions / complaints please reach out to us by creating a Question issue at +(https://jira.mongodb.org/secure/CreateIssue!default.jspa). diff --git a/README.md b/README.md index ec6eb66..422778a 100644 --- a/README.md +++ b/README.md @@ -1 +1,111 @@ -# mongo-spring-session \ No newline at end of file +# MongoDB Spring Session extension + +This product enables applications to use [Spring Session](https://spring.io/projects/spring-session) +with [MongoDB](https://www.mongodb.com/) and provides a `SessionRepository` implementation backed +by MongoDB using [Spring Data MongoDB](https://spring.io/projects/spring-data-mongodb). + +## Overview + +Spring Session provides an API and implementations for managing a user's session information, +while also making it trivial to support clustered sessions without being tied to an application container +specific solution. It also provides transparent integration with: + + * `HttpSession` - allows replacing the `HttpSession` in an application container (i.e. Tomcat) neutral way, with support for providing session IDs in headers to work with RESTful APIs. + * `WebSocket` - provides the ability to keep the `HttpSession` alive when receiving WebSocket messages + * `WebSession` - allows replacing the Spring WebFlux's `WebSession` in an application container neutral way. + +## Migrating from `spring-session-data-mongodb` + +The API namespace has changed from `org.springframework.session.data.mongo` to `org.mongodb.spring.session`. +The rest of the classes remain the same. + +## Support / Feedback + +For issues with, questions about, or feedback for the MongoDB Java, Kotlin, and Scala drivers, please look into +our [support channels](https://www.mongodb.com/docs/manual/support/). Please +do not email any of the driver developers directly with issues or +questions - you're more likely to get an answer on [StackOverflow](https://stackoverflow.com/questions/tagged/mongodb+java). + +At a minimum, please include in your description the exact version of the library and any dependencies that you are using. + +## Bugs / Feature Requests + +Think you’ve found a bug? Want to see a new feature in the MongoDB Spring Session? Please open a +case in our issue management tool, JIRA: + +- [Create an account and login](https://jira.mongodb.org). +- Navigate to [the JAVA Frameworks project](https://jira.mongodb.org/browse/JAVAF). +- Click **Create Issue** - Please provide as much information as possible about the issue type, which driver you are using, and how to reproduce your issue. + +Bug reports in JIRA for the extension and the Core Server (i.e. SERVER) project are **public**. + +If you’ve identified a security vulnerability in the library or any other +MongoDB project, please report it according to the [instructions here](https://www.mongodb.com/docs/manual/tutorial/create-a-vulnerability-report). + +## Versioning + +We follow [semantic versioning](https://semver.org/spec/v2.0.0.html) when releasing. + +## Binaries + +Binaries and dependency information for Maven, Gradle, Ivy and others can be found at +[https://central.sonatype.com/search](https://central.sonatype.com/search?namespace=org.mongodb&name=mongo-spring-session). + +Example for Maven: + +```xml + + org.mongodb + mongo-spring-session + x.y.z + +``` +Snapshot builds are also published regulary via Sonatype. + +Example for Maven: + +```xml + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + +``` + +### Testing + +This project uses separate directories for unit and integration tests: + +- [unit test](src/test) +- [integration test](src/integrationTest) + +#### Gradle Tasks + +##### All checks +```console +./gradlew clean check +``` + +##### Unit Tests only +```console +./gradlew clean test +``` + +##### Integration Tests only +```console +./gradlew clean integrationTest +``` + +Integration tests require a MongoDB deployment to be available + +### CI/CD +This project uses [evergreen](https://github.com/evergreen-ci/evergreen), a distributed continuous integration system from MongoDB. +The evergreen configuration is in the [.evergreen](/.evergreen) directory. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..7abe876 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,227 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.adarshr.gradle.testlogger.theme.ThemeType +import net.ltgt.gradle.errorprone.errorprone + +buildscript { + repositories { + gradlePluginPortal() + maven(url = "https://repo.spring.io/plugins-release/") + } +} + +plugins { + id("eclipse") + id("idea") + id("java-library") + id("maven-publish") + alias(libs.plugins.spotless) + alias(libs.plugins.test.logger) + alias(libs.plugins.errorprone) +} + +description = "Spring Session and Spring MongoDB integration" + +repositories { + mavenLocal() + mavenCentral() +} + +java { + toolchain { languageVersion = JavaLanguageVersion.of(17) } // Remember to update javadoc links + withJavadocJar() + withSourcesJar() + registerFeature("optional") { usingSourceSet(sourceSets["main"]) } +} + +// Suppress POM warnings for the optional features (eg: optionalApi, optionalImplementation) +// afterEvaluate { +// configurations +// .filter { it.name.startsWith("optional") } +// .forEach { optional -> +// publishing.publications.named("maven") { suppressPomMetadataWarningsFor(optional.name) } +// } +// } + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Integration Test + +// Added `Action` explicitly due to an intellij 2025.2 false positive: https://youtrack.jetbrains.com/issue/KTIJ-34210 +sourceSets { + create( + "integrationTest", + Action { + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().output + }) +} + +val integrationTestSourceSet: SourceSet = sourceSets["integrationTest"] + +val integrationTestImplementation: Configuration by + configurations.getting { + extendsFrom( + configurations.getByName("api"), + configurations.getByName("optionalApi"), + configurations.getByName("implementation"), + configurations.getByName("optionalImplementation"), + configurations.getByName("testImplementation")) + } +val integrationTestRuntimeOnly: Configuration by + configurations.getting { + extendsFrom(configurations.getByName("runtimeOnly"), configurations.getByName("testRuntimeOnly")) + } + +val integrationTestTask = + tasks.register("integrationTest") { + group = LifecycleBasePlugin.VERIFICATION_GROUP + testClassesDirs = integrationTestSourceSet.output.classesDirs + classpath = integrationTestSourceSet.runtimeClasspath + } + +tasks.check { dependsOn(integrationTestTask) } + +tasks.withType().configureEach { + useJUnitPlatform() + + // Pass any `org.mongodb.*` system settings + systemProperties = + System.getProperties() + .map { (key, value) -> Pair(key.toString(), value) } + .filter { it.first.startsWith("org.mongodb.") } + .toMap() +} + +// Pretty test output +testlogger { + theme = ThemeType.STANDARD + showExceptions = true + showStackTraces = true + showFullStackTraces = false + showCauses = true + slowThreshold = 2000 + showSummary = true + showSimpleNames = false + showPassed = true + showSkipped = true + showFailed = true + showOnlySlow = false + showStandardStreams = false + showPassedStandardStreams = true + showSkippedStandardStreams = true + showFailedStandardStreams = true + logLevel = LogLevel.LIFECYCLE +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Dependencies + +dependencies { + api(platform(libs.spring.session.bom)) + api(libs.spring.session.core) + + api(platform(libs.spring.data.bom)) + api(libs.spring.data.mongodb) + + api(platform(libs.jackson.bom)) + api(libs.jackson.databind) + + api(platform(libs.spring.security.bom)) + api(libs.spring.security.core) + + // Optional dependencies + "optionalApi"(platform(libs.project.reactor.bom)) + "optionalApi"(libs.project.reactor.core) + + implementation(platform(libs.mongodb.driver.bom)) + implementation(libs.mongodb.driver.core) + "optionalImplementation"(libs.mongodb.driver.sync) + "optionalImplementation"(libs.mongodb.driver.reactive.streams) + + // We need the `libs.findbugs.jsr` dependency to stop `javadoc` from emitting + // `warning: unknown enum constant When.MAYBE` + // `reason: class file for javax.annotation.meta.When not found`. + compileOnly(libs.findbugs.jsr) + errorprone(libs.nullaway) + errorprone(libs.google.errorprone.core) + + testImplementation(platform(libs.junit.bom)) + testImplementation(platform(libs.mockito.bom)) + testImplementation(platform(libs.spring.framework.bom)) + + testImplementation(libs.bundles.testing) + testImplementation(libs.bundles.jakarta) + testImplementation(libs.bundles.spring.test) + testImplementation(libs.logback.core) + testImplementation(libs.project.reactor.test) + + testRuntimeOnly(libs.junit.platform.launcher) +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Static Analysis + +tasks.named("check") { dependsOn("spotlessApply") } + +spotless { + java { + importOrder() + + removeUnusedImports() + + palantirJavaFormat(libs.versions.plugin.palantir.get()).formatJavadoc(true) + + formatAnnotations() + + // need to add license header manually to package-info.java and module-info.java + // due to the bug: https://github.com/diffplug/spotless/issues/532 + licenseHeaderFile(file("config/spotless.license.java")) + + targetExclude("build/generated/sources/buildConfig/**/*.java") + } + + kotlinGradle { + ktfmt(libs.versions.plugin.ktfmt.get()).configure { + it.setMaxWidth(120) + it.setBlockIndent(4) + } + trimTrailingWhitespace() + leadingTabsToSpaces() + endWithNewline() + } + + format("extraneous") { + target("*.xml", "*.yml", "*.md", "*.toml") + trimTrailingWhitespace() + leadingTabsToSpaces() + endWithNewline() + } +} + +// Configure errorprone +tasks.withType().configureEach { + if (name.endsWith("TestJava")) { + options.errorprone.isEnabled = false + } else { + options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror")) + options.errorprone { + disableWarningsInGeneratedCode = true + option("NullAway:AnnotatedPackages", "org.mongodb.spring.session") + error("NullAway") + } + } +} diff --git a/config/spotless.license.java b/config/spotless.license.java new file mode 100644 index 0000000..f401343 --- /dev/null +++ b/config/spotless.license.java @@ -0,0 +1,17 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2c6f853 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Copyright 2025-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +version=0.1.0-SNAPSHOT + +org.gradle.jvmargs=-Duser.country=US -Duser.language=en -Dfile.encoding=UTF-8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..16dfa3c --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,99 @@ +# Copyright 2025-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[versions] +spring-session = "4.0.0-RC1" +spring-data = "2025.0.5" +spring-security = "6.5.6" +spring-framework = "6.2.12" +jackson = "2.18.4" +project-reactor = "2025.0.0" +mongodb-driver = "5.6.1" + +# Code verification libraries +findbugs = "3.0.2" +nullaway = "0.12.4" +google-errorprone-core = "2.36.0" + +# Test libraries +assertj = "3.27.3" +junit = "5.12.1" +hamcrest = "3.0" +jakarta-websocket = "2.1.1" +jakarta-servlet-api = "6.0.0" +mockito = "5.16.1" +logback = "1.5.18" + +plugin-spotless = "7.0.2" +plugin-palantir = "2.58.0" +plugin-ktfmt = "0.54" +plugin-test-logger = "4.0.0" +plugin-errorprone = "4.1.0" + +[libraries] +spring-session-bom = { module = "org.springframework.session:spring-session-bom", version.ref = "spring-session" } +spring-session-core = { module = "org.springframework.session:spring-session-core" } +spring-data-bom = { module = "org.springframework.data:spring-data-bom", version.ref = "spring-data" } +spring-data-mongodb = { module = "org.springframework.data:spring-data-mongodb" } +jackson-bom = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "jackson" } +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind" } + +spring-security-bom = { module = "org.springframework.security:spring-security-bom", version.ref = "spring-security" } +spring-security-core = { module = "org.springframework.security:spring-security-core" } + +project-reactor-bom = { module = "io.projectreactor:reactor-bom", version.ref = "project-reactor" } +project-reactor-core = { module = "io.projectreactor:reactor-core" } +project-reactor-test = { module = "io.projectreactor:reactor-test" } + +mongodb-driver-bom = { module = "org.mongodb:mongodb-driver-bom", version.ref = "mongodb-driver" } +mongodb-driver-core = { module = "org.mongodb:mongodb-driver-core" } +mongodb-driver-sync = { module = "org.mongodb:mongodb-driver-sync" } +mongodb-driver-reactive-streams = { module = "org.mongodb:mongodb-driver-reactivestreams" } + +# Code verification libraries +findbugs-jsr = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs" } +nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway" } +google-errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "google-errorprone-core" } + +# Test libraries +junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } +mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "mockito" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +mockito-core = { module = "org.mockito:mockito-core" } +mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter" } +assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } +hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } +logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" } + +jakarta-websocket-api = { module = "jakarta.websocket:jakarta.websocket-api", version.ref = "jakarta-websocket" } +jakarta-websocket-client-api = { module = "jakarta.websocket:jakarta.websocket-client-api", version.ref = "jakarta-websocket" } +jakarta-servlet-api = { module = "jakarta.servlet:jakarta.servlet-api", version.ref = "jakarta-servlet-api" } + +spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "spring-framework" } +spring-security-config = { module = "org.springframework.security:spring-security-config" } +spring-security-web = { module = "org.springframework.security:spring-security-web" } +spring-test = { module = "org.springframework:spring-test" } +spring-web = { module = "org.springframework:spring-web" } +spring-webflux = { module = "org.springframework:spring-webflux" } + +[bundles] +testing = ["junit-jupiter", "assertj", "hamcrest", "mockito-core", "mockito-junit-jupiter"] +jakarta = ["jakarta-servlet-api", "jakarta-websocket-client-api", "jakarta-websocket-api"] +spring-test = ["spring-security-config", "spring-security-web", "spring-test", "spring-web", "spring-webflux"] + +[plugins] +spotless = { id = "com.diffplug.spotless", version.ref = "plugin-spotless" } +test-logger = { id = "com.adarshr.test-logger", version.ref = "plugin-test-logger" } +errorprone = { id = "net.ltgt.errorprone", version.ref = "plugin-errorprone" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bad7c24 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..8d92201 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +rootProject.name = "mongo-spring-session" diff --git a/spring-session-data-mongodb.gradle b/spring-session-data-mongodb.gradle deleted file mode 100644 index 1a82382..0000000 --- a/spring-session-data-mongodb.gradle +++ /dev/null @@ -1,49 +0,0 @@ -apply plugin: 'io.spring.convention.spring-module' - -description = "Spring Session and Spring MongoDB integration" - -dependencies { - management platform(project(":spring-session-dependencies")) - - api project(':spring-session-core') - - // Spring Data MongoDB - - api("org.springframework.data:spring-data-mongodb") { - exclude group: "org.mongodb", module: "mongo-java-driver" - exclude group: "org.slf4j", module: "jcl-over-slf4j" - } - - // MongoDB dependencies - - optional "org.mongodb:mongodb-driver-core" - testImplementation "org.mongodb:mongodb-driver-reactivestreams" - testImplementation "org.mongodb:mongodb-driver-sync" - testImplementation 'jakarta.websocket:jakarta.websocket-api' - testImplementation 'jakarta.websocket:jakarta.websocket-client-api' - integrationTestCompile "org.testcontainers:mongodb" - - // Everything else - - api "com.fasterxml.jackson.core:jackson-databind" - api "com.google.code.findbugs:jsr305" - api "org.springframework.security:spring-security-core" - - optional "io.projectreactor:reactor-core" - - testImplementation "ch.qos.logback:logback-core" - testImplementation "io.projectreactor:reactor-test" - testImplementation "jakarta.servlet:jakarta.servlet-api" - testImplementation "org.assertj:assertj-core" - testImplementation "org.hamcrest:hamcrest" - testImplementation "org.junit.jupiter:junit-jupiter-engine" - testImplementation "org.junit.jupiter:junit-jupiter-params" - testImplementation "org.mockito:mockito-core" - testImplementation "org.mockito:mockito-junit-jupiter" - testImplementation "org.springframework.security:spring-security-config" - testImplementation "org.springframework.security:spring-security-web" - testImplementation "org.springframework:spring-test" - testImplementation "org.springframework:spring-web" - testImplementation "org.springframework:spring-webflux" - testRuntimeOnly "org.junit.platform:junit-platform-launcher" -} diff --git a/src/integration-test/java/org/springframework/session/data/mongo/AbstractClassLoaderTest.java b/src/integration-test/java/org/springframework/session/data/mongo/AbstractClassLoaderTest.java deleted file mode 100644 index 59cb467..0000000 --- a/src/integration-test/java/org/springframework/session/data/mongo/AbstractClassLoaderTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.lang.reflect.Field; - -import org.assertj.core.api.AssertionsForClassTypes; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.core.serializer.DefaultDeserializer; -import org.springframework.core.serializer.support.DeserializingConverter; -import org.springframework.util.ReflectionUtils; - -/** - * Verify container's {@link ClassLoader} is injected into session converter (reactive and - * traditional). - * - * @author Greg Turnquist - */ -public abstract class AbstractClassLoaderTest extends AbstractITest { - - @Autowired - T sessionRepository; - - @Autowired - ApplicationContext applicationContext; - - @Test - void verifyContainerClassLoaderLoadedIntoConverter() { - - Field mongoSessionConverterField = ReflectionUtils.findField(this.sessionRepository.getClass(), - "mongoSessionConverter"); - ReflectionUtils.makeAccessible(mongoSessionConverterField); - AbstractMongoSessionConverter sessionConverter = (AbstractMongoSessionConverter) ReflectionUtils - .getField(mongoSessionConverterField, this.sessionRepository); - - AssertionsForClassTypes.assertThat(sessionConverter).isInstanceOf(JdkMongoSessionConverter.class); - - JdkMongoSessionConverter jdkMongoSessionConverter = (JdkMongoSessionConverter) sessionConverter; - - DeserializingConverter deserializingConverter = (DeserializingConverter) extractField( - JdkMongoSessionConverter.class, "deserializer", jdkMongoSessionConverter); - DefaultDeserializer deserializer = (DefaultDeserializer) extractField(DeserializingConverter.class, - "deserializer", deserializingConverter); - ClassLoader classLoader = (ClassLoader) extractField(DefaultDeserializer.class, "classLoader", deserializer); - - AssertionsForClassTypes.assertThat(classLoader).isEqualTo(this.applicationContext.getClassLoader()); - } - - private static Object extractField(Class clazz, String fieldName, Object obj) { - - Field field = ReflectionUtils.findField(clazz, fieldName); - ReflectionUtils.makeAccessible(field); - return ReflectionUtils.getField(field, obj); - } - -} diff --git a/src/integration-test/java/org/springframework/session/data/mongo/AbstractMongoRepositoryITest.java b/src/integration-test/java/org/springframework/session/data/mongo/AbstractMongoRepositoryITest.java deleted file mode 100644 index 7efae90..0000000 --- a/src/integration-test/java/org/springframework/session/data/mongo/AbstractMongoRepositoryITest.java +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.time.Duration; -import java.time.Instant; -import java.util.Map; -import java.util.UUID; - -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.MongoDBContainer; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.session.FindByIndexNameSessionRepository; -import org.springframework.session.Session; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Abstract base class for {@link MongoIndexedSessionRepository} tests. - * - * @author Jakub Kubrynski - * @author Vedran Pavic - * @author Greg Turnquist - */ -public abstract class AbstractMongoRepositoryITest extends AbstractITest { - - protected static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; - - protected static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; - - @Autowired - protected MongoIndexedSessionRepository repository; - - @Test - void saves() { - - String username = "saves-" + System.currentTimeMillis(); - - MongoSession toSave = this.repository.createSession(); - String expectedAttributeName = "a"; - String expectedAttributeValue = "b"; - toSave.setAttribute(expectedAttributeName, expectedAttributeValue); - Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username, "password", - AuthorityUtils.createAuthorityList("ROLE_USER")); - SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext(); - toSaveContext.setAuthentication(toSaveToken); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, toSaveContext); - toSave.setAttribute(INDEX_NAME, username); - - this.repository.save(toSave); - - Session session = this.repository.findById(toSave.getId()); - - assertThat(session.getId()).isEqualTo(toSave.getId()); - assertThat(session.getAttributeNames()).isEqualTo(toSave.getAttributeNames()); - assertThat(session.getAttribute(expectedAttributeName)) - .isEqualTo(toSave.getAttribute(expectedAttributeName)); - - this.repository.deleteById(toSave.getId()); - - String id = toSave.getId(); - assertThat(this.repository.findById(id)).isNull(); - } - - @Test - void putAllOnSingleAttrDoesNotRemoveOld() { - - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute("a", "b"); - - this.repository.save(toSave); - toSave = this.repository.findById(toSave.getId()); - - toSave.setAttribute("1", "2"); - - this.repository.save(toSave); - toSave = this.repository.findById(toSave.getId()); - - Session session = this.repository.findById(toSave.getId()); - assertThat(session.getAttributeNames().size()).isEqualTo(2); - assertThat(session.getAttribute("a")).isEqualTo("b"); - assertThat(session.getAttribute("1")).isEqualTo("2"); - - this.repository.deleteById(toSave.getId()); - } - - @Test - void findByPrincipalName() throws Exception { - - String principalName = "findByPrincipalName" + UUID.randomUUID(); - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(INDEX_NAME, principalName); - - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - principalName); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - - this.repository.deleteById(toSave.getId()); - - findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName); - - assertThat(findByPrincipalName).hasSize(0); - assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId()); - } - - @Test - void nonExistentSessionShouldNotBreakMongo() { - this.repository.deleteById("doesn't exist"); - } - - @Test - void findByPrincipalNameNoPrincipalNameChange() throws Exception { - - String principalName = "findByPrincipalNameNoPrincipalNameChange" + UUID.randomUUID(); - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(INDEX_NAME, principalName); - - this.repository.save(toSave); - - toSave.setAttribute("other", "value"); - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - principalName); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - } - - @Test - void findByPrincipalNameNoPrincipalNameChangeReload() throws Exception { - - String principalName = "findByPrincipalNameNoPrincipalNameChangeReload" + UUID.randomUUID(); - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(INDEX_NAME, principalName); - - this.repository.save(toSave); - - toSave = this.repository.findById(toSave.getId()); - - toSave.setAttribute("other", "value"); - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - principalName); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - } - - @Test - void findByDeletedPrincipalName() throws Exception { - - String principalName = "findByDeletedPrincipalName" + UUID.randomUUID(); - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(INDEX_NAME, principalName); - - this.repository.save(toSave); - - toSave.setAttribute(INDEX_NAME, null); - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - principalName); - - assertThat(findByPrincipalName).isEmpty(); - } - - @Test - void findByChangedPrincipalName() throws Exception { - - String principalName = "findByChangedPrincipalName" + UUID.randomUUID(); - String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID(); - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(INDEX_NAME, principalName); - - this.repository.save(toSave); - - toSave.setAttribute(INDEX_NAME, principalNameChanged); - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - principalName); - assertThat(findByPrincipalName).isEmpty(); - - findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - } - - @Test - void findByDeletedPrincipalNameReload() throws Exception { - - String principalName = "findByDeletedPrincipalName" + UUID.randomUUID(); - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(INDEX_NAME, principalName); - - this.repository.save(toSave); - - MongoSession getSession = this.repository.findById(toSave.getId()); - getSession.setAttribute(INDEX_NAME, null); - this.repository.save(getSession); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - principalName); - - assertThat(findByPrincipalName).isEmpty(); - } - - @Test - void findByChangedPrincipalNameReload() throws Exception { - - String principalName = "findByChangedPrincipalName" + UUID.randomUUID(); - String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID(); - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(INDEX_NAME, principalName); - - this.repository.save(toSave); - - MongoSession getSession = this.repository.findById(toSave.getId()); - - getSession.setAttribute(INDEX_NAME, principalNameChanged); - this.repository.save(getSession); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - principalName); - assertThat(findByPrincipalName).isEmpty(); - - findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - } - - @Test - void findBySecurityPrincipalName() throws Exception { - - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); - - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - getSecurityName()); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - - this.repository.deleteById(toSave.getId()); - - findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); - - assertThat(findByPrincipalName).hasSize(0); - assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId()); - } - - @Test - void findByPrincipalNameNoSecurityPrincipalNameChange() throws Exception { - - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); - - this.repository.save(toSave); - - toSave.setAttribute("other", "value"); - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - getSecurityName()); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - } - - @Test - void findByDeletedSecurityPrincipalName() throws Exception { - - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); - - this.repository.save(toSave); - - toSave.setAttribute(SPRING_SECURITY_CONTEXT, null); - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - getSecurityName()); - - assertThat(findByPrincipalName).isEmpty(); - } - - @Test - void findByChangedSecurityPrincipalName() throws Exception { - - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); - - this.repository.save(toSave); - - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext); - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - getSecurityName()); - assertThat(findByPrincipalName).isEmpty(); - - findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName()); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - } - - @Test - void findByChangedSecurityPrincipalNameReload() throws Exception { - - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); - - this.repository.save(toSave); - - MongoSession getSession = this.repository.findById(toSave.getId()); - - getSession.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext); - this.repository.save(getSession); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - getSecurityName()); - assertThat(findByPrincipalName).isEmpty(); - - findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName()); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - } - - @Test - void loadExpiredSession() throws Exception { - - // given - MongoSession expiredSession = this.repository.createSession(); - Instant thirtyOneMinutesAgo = Instant.ofEpochMilli(System.currentTimeMillis()).minus(Duration.ofMinutes(31)); - expiredSession.setLastAccessedTime(thirtyOneMinutesAgo); - this.repository.save(expiredSession); - - // then - MongoSession expiredSessionFromDb = this.repository.findById(expiredSession.getId()); - assertThat(expiredSessionFromDb).isNull(); - } - - protected String getSecurityName() { - return this.context.getAuthentication().getName(); - } - - protected String getChangedSecurityName() { - return this.changedContext.getAuthentication().getName(); - } - - protected static class BaseConfig { - - private static final String DOCKER_IMAGE = "mongo:5.0.11"; - - @Bean - public MongoDBContainer mongoDbContainer() { - MongoDBContainer mongoDbContainer = new MongoDBContainer(DOCKER_IMAGE); - mongoDbContainer.start(); - return mongoDbContainer; - } - - @Bean - public MongoOperations mongoOperations(MongoDBContainer mongoContainer) { - - MongoClient mongo = MongoClients - .create("mongodb://" + mongoContainer.getHost() + ":" + mongoContainer.getFirstMappedPort()); - return new MongoTemplate(mongo, "test"); - } - - } - -} diff --git a/src/integration-test/java/org/springframework/session/data/mongo/MongoDbDeleteJacksonSessionVerificationTest.java b/src/integration-test/java/org/springframework/session/data/mongo/MongoDbDeleteJacksonSessionVerificationTest.java deleted file mode 100644 index e8af0a9..0000000 --- a/src/integration-test/java/org/springframework/session/data/mongo/MongoDbDeleteJacksonSessionVerificationTest.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.net.URI; - -import com.mongodb.reactivestreams.client.MongoClient; -import com.mongodb.reactivestreams.client.MongoClients; -import org.assertj.core.api.AssertionsForClassTypes; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.testcontainers.containers.MongoDBContainer; -import reactor.test.StepVerifier; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.mongodb.core.ReactiveMongoOperations; -import org.springframework.data.mongodb.core.ReactiveMongoTemplate; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; -import org.springframework.security.config.web.server.ServerHttpSecurity; -import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.session.data.mongo.config.annotation.web.reactive.EnableMongoWebSession; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.reactive.server.FluxExchangeResult; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.reactive.function.BodyInserters; - -/** - * @author Boris Finkelshteyn - */ -@ExtendWith(SpringExtension.class) -@ContextConfiguration -class MongoDbDeleteJacksonSessionVerificationTest { - - @Autowired - ApplicationContext ctx; - - WebTestClient client; - - @BeforeEach - void setUp() { - this.client = WebTestClient.bindToApplicationContext(this.ctx).build(); - } - - @Test - void logoutShouldDeleteOldSessionFromMongoDB() { - - // 1. Login and capture the SESSION cookie value. - // @formatter:off - FluxExchangeResult loginResult = this.client.post().uri("/login") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body(BodyInserters - .fromFormData("username", "admin") - .with("password", "password")) - .exchange() - .returnResult(String.class); - // @formatter:on - - AssertionsForClassTypes.assertThat(loginResult.getResponseHeaders().getLocation()).isEqualTo(URI.create("/")); - - String originalSessionId = loginResult.getResponseCookies().getFirst("SESSION").getValue(); - - // 2. Fetch a protected resource using the SESSION cookie. - // @formatter:off - this.client.get().uri("/hello") - .cookie("SESSION", originalSessionId) - .exchange() - .expectStatus().isOk() - .returnResult(String.class).getResponseBody() - .as(StepVerifier::create) - .expectNext("HelloWorld") - .verifyComplete(); - // @formatter:on - - // 3. Logout using the SESSION cookie, and capture the new SESSION cookie. - // @formatter:off - String newSessionId = this.client.post().uri("/logout") - .cookie("SESSION", originalSessionId) - .exchange() - .expectStatus().isFound() - .returnResult(String.class).getResponseCookies().getFirst("SESSION").getValue(); - // @formatter:on - - AssertionsForClassTypes.assertThat(newSessionId).isNotEqualTo(originalSessionId); - - // 4. Verify the new SESSION cookie is not yet authorized. - // @formatter:off - this.client.get().uri("/hello") - .cookie("SESSION", newSessionId) - .exchange() - .expectStatus().isFound() - .expectHeader() - .value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value).isEqualTo("/login")); - // @formatter:on - - // 5. Verify the original SESSION cookie no longer works. - // @formatter:off - this.client.get().uri("/hello") - .cookie("SESSION", originalSessionId) - .exchange() - .expectStatus().isFound() - .expectHeader() - .value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value).isEqualTo("/login")); - // @formatter:on - } - - @RestController - static class TestController { - - @GetMapping("/hello") - ResponseEntity hello() { - return ResponseEntity.ok("HelloWorld"); - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableWebFluxSecurity - static class SecurityConfig { - - @Bean - SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - // @formatter:off - return http - .logout(Customizer.withDefaults()) - .formLogin(Customizer.withDefaults()) - .csrf((csrf) -> csrf.disable()) - .authorizeExchange((ae) -> ae.anyExchange().authenticated()) - .build(); - // @formatter:on - } - - @Bean - MapReactiveUserDetailsService userDetailsService() { - // @formatter:off - return new MapReactiveUserDetailsService(User.withUsername("admin") - .password("{noop}password") - .roles("USER,ADMIN") - .build()); - // @formatter:on - } - - @Bean - AbstractMongoSessionConverter mongoSessionConverter() { - return new JacksonMongoSessionConverter(); - } - - } - - @Configuration - @EnableWebFlux - @EnableMongoWebSession - static class Config { - - private static final String DOCKER_IMAGE = "mongo:5.0.11"; - - @Bean - MongoDBContainer mongoDbContainer() { - MongoDBContainer mongoDbContainer = new MongoDBContainer(DOCKER_IMAGE); - mongoDbContainer.start(); - return mongoDbContainer; - } - - @Bean - ReactiveMongoOperations mongoOperations(MongoDBContainer mongoContainer) { - - MongoClient mongo = MongoClients - .create("mongodb://" + mongoContainer.getHost() + ":" + mongoContainer.getFirstMappedPort()); - return new ReactiveMongoTemplate(mongo, "DB_Name_DeleteJacksonSessionVerificationTest"); - } - - @Bean - TestController controller() { - return new TestController(); - } - - } - -} diff --git a/src/integration-test/java/org/springframework/session/data/mongo/MongoDbLogoutVerificationTest.java b/src/integration-test/java/org/springframework/session/data/mongo/MongoDbLogoutVerificationTest.java deleted file mode 100644 index 8ce9bdd..0000000 --- a/src/integration-test/java/org/springframework/session/data/mongo/MongoDbLogoutVerificationTest.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.net.URI; - -import com.mongodb.reactivestreams.client.MongoClient; -import com.mongodb.reactivestreams.client.MongoClients; -import org.assertj.core.api.AssertionsForClassTypes; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.testcontainers.containers.MongoDBContainer; -import reactor.test.StepVerifier; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.mongodb.core.ReactiveMongoOperations; -import org.springframework.data.mongodb.core.ReactiveMongoTemplate; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; -import org.springframework.security.config.web.server.ServerHttpSecurity; -import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.session.data.mongo.config.annotation.web.reactive.EnableMongoWebSession; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.reactive.server.FluxExchangeResult; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.reactive.function.BodyInserters; - -/** - * @author Greg Turnquist - */ -@ExtendWith(SpringExtension.class) -@ContextConfiguration -class MongoDbLogoutVerificationTest { - - @Autowired - ApplicationContext ctx; - - WebTestClient client; - - @BeforeEach - void setUp() { - this.client = WebTestClient.bindToApplicationContext(this.ctx).build(); - } - - @Test - void logoutShouldDeleteOldSessionFromMongoDB() { - - // 1. Login and capture the SESSION cookie value. - - FluxExchangeResult loginResult = this.client.post() - .uri("/login") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) // - .body(BodyInserters // - .fromFormData("username", "admin") // - .with("password", "password")) // - .exchange() // - .returnResult(String.class); - - AssertionsForClassTypes.assertThat(loginResult.getResponseHeaders().getLocation()).isEqualTo(URI.create("/")); - - String originalSessionId = loginResult.getResponseCookies().getFirst("SESSION").getValue(); - - // 2. Fetch a protected resource using the SESSION cookie. - - this.client.get() - .uri("/hello") // - .cookie("SESSION", originalSessionId) // - .exchange() // - .expectStatus() - .isOk() // - .returnResult(String.class) - .getResponseBody() // - .as(StepVerifier::create) // - .expectNext("HelloWorld") // - .verifyComplete(); - - // 3. Logout using the SESSION cookie, and capture the new SESSION cookie. - - String newSessionId = this.client.post() - .uri("/logout") // - .cookie("SESSION", originalSessionId) // - .exchange() // - .expectStatus() - .isFound() // - .returnResult(String.class) - .getResponseCookies() - .getFirst("SESSION") - .getValue(); - - AssertionsForClassTypes.assertThat(newSessionId).isNotEqualTo(originalSessionId); - - // 4. Verify the new SESSION cookie is not yet authorized. - - this.client.get() - .uri("/hello") // - .cookie("SESSION", newSessionId) // - .exchange() // - .expectStatus() - .isFound() // - .expectHeader() - .value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value).isEqualTo("/login")); - - // 5. Verify the original SESSION cookie no longer works. - - this.client.get() - .uri("/hello") // - .cookie("SESSION", originalSessionId) // - .exchange() // - .expectStatus() - .isFound() // - .expectHeader() - .value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value).isEqualTo("/login")); - } - - @RestController - static class TestController { - - @GetMapping("/hello") - ResponseEntity hello() { - return ResponseEntity.ok("HelloWorld"); - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableWebFluxSecurity - static class SecurityConfig { - - @Bean - SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - // @formatter:off - return http - .logout(Customizer.withDefaults()) - .formLogin(Customizer.withDefaults()) - .csrf((csrf) -> csrf.disable()) - .authorizeExchange((ae) -> ae.anyExchange().authenticated()) - .build(); - // @formatter:on - } - - @Bean - MapReactiveUserDetailsService userDetailsService() { - // @formatter:off - return new MapReactiveUserDetailsService(User.withUsername("admin") - .password("{noop}password") - .roles("USER,ADMIN") - .build()); - // @formatter:on - } - - } - - @Configuration - @EnableWebFlux - @EnableMongoWebSession - static class Config { - - private static final String DOCKER_IMAGE = "mongo:5.0.11"; - - @Bean - MongoDBContainer mongoDbContainer() { - MongoDBContainer mongoDbContainer = new MongoDBContainer(DOCKER_IMAGE); - mongoDbContainer.start(); - return mongoDbContainer; - } - - @Bean - ReactiveMongoOperations mongoOperations(MongoDBContainer mongoContainer) { - - MongoClient mongo = MongoClients - .create("mongodb://" + mongoContainer.getHost() + ":" + mongoContainer.getFirstMappedPort()); - return new ReactiveMongoTemplate(mongo, "test"); - } - - @Bean - TestController controller() { - return new TestController(); - } - - } - -} diff --git a/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJdkSerializationITest.java b/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJdkSerializationITest.java deleted file mode 100644 index db242ef..0000000 --- a/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJdkSerializationITest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.time.Duration; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession; -import org.springframework.test.context.ContextConfiguration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for - * {@link org.springframework.session.data.mongo.MongoIndexedSessionRepository} that use - * {@link JdkMongoSessionConverter} based session serialization. - * - * @author Jakub Kubrynski - * @author Vedran Pavic - * @author Greg Turnquist - */ -@ContextConfiguration -class MongoRepositoryJdkSerializationITest extends AbstractMongoRepositoryITest { - - @Test - void findByDeletedSecurityPrincipalNameReload() throws Exception { - - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); - - this.repository.save(toSave); - - MongoSession getSession = this.repository.findById(toSave.getId()); - getSession.setAttribute(INDEX_NAME, null); - this.repository.save(getSession); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - getChangedSecurityName()); - - assertThat(findByPrincipalName).isEmpty(); - } - - @Test - void findByPrincipalNameNoSecurityPrincipalNameChangeReload() throws Exception { - - MongoSession toSave = this.repository.createSession(); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); - - this.repository.save(toSave); - - toSave = this.repository.findById(toSave.getId()); - - toSave.setAttribute("other", "value"); - this.repository.save(toSave); - - Map findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, - getSecurityName()); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - } - - // tag::sample[] - @Configuration - @EnableMongoHttpSession - static class Config extends BaseConfig { - - @Bean - AbstractMongoSessionConverter mongoSessionConverter() { - return new JdkMongoSessionConverter(Duration.ofMinutes(30)); - } - - } - // end::sample[] - -} diff --git a/src/integrationTest/java/org/mongodb/spring/session/AbstractClassLoaderTest.java b/src/integrationTest/java/org/mongodb/spring/session/AbstractClassLoaderTest.java new file mode 100644 index 0000000..2ea0766 --- /dev/null +++ b/src/integrationTest/java/org/mongodb/spring/session/AbstractClassLoaderTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import java.lang.reflect.Field; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.serializer.DefaultDeserializer; +import org.springframework.core.serializer.support.DeserializingConverter; +import org.springframework.util.ReflectionUtils; + +/** + * Verify container's {@link ClassLoader} is injected into session converter (reactive and traditional). + * + * @author Greg Turnquist + */ +public abstract class AbstractClassLoaderTest extends AbstractITest { + + @Autowired + T sessionRepository; + + @Autowired + ApplicationContext applicationContext; + + @Test + void verifyContainerClassLoaderLoadedIntoConverter() { + + Field mongoSessionConverterField = + ReflectionUtils.findField(this.sessionRepository.getClass(), "mongoSessionConverter"); + ReflectionUtils.makeAccessible(mongoSessionConverterField); + AbstractMongoSessionConverter sessionConverter = (AbstractMongoSessionConverter) + ReflectionUtils.getField(mongoSessionConverterField, this.sessionRepository); + + AssertionsForClassTypes.assertThat(sessionConverter).isInstanceOf(JdkMongoSessionConverter.class); + + JdkMongoSessionConverter jdkMongoSessionConverter = (JdkMongoSessionConverter) sessionConverter; + + DeserializingConverter deserializingConverter = (DeserializingConverter) + extractField(JdkMongoSessionConverter.class, "deserializer", jdkMongoSessionConverter); + DefaultDeserializer deserializer = (DefaultDeserializer) + extractField(DeserializingConverter.class, "deserializer", deserializingConverter); + ClassLoader classLoader = (ClassLoader) extractField(DefaultDeserializer.class, "classLoader", deserializer); + + AssertionsForClassTypes.assertThat(classLoader).isEqualTo(this.applicationContext.getClassLoader()); + } + + private static Object extractField(Class clazz, String fieldName, Object obj) { + + Field field = ReflectionUtils.findField(clazz, fieldName); + ReflectionUtils.makeAccessible(field); + return ReflectionUtils.getField(field, obj); + } +} diff --git a/src/integration-test/java/org/springframework/session/data/mongo/AbstractITest.java b/src/integrationTest/java/org/mongodb/spring/session/AbstractITest.java similarity index 58% rename from src/integration-test/java/org/springframework/session/data/mongo/AbstractITest.java rename to src/integrationTest/java/org/mongodb/spring/session/AbstractITest.java index 592fa81..a95a737 100644 --- a/src/integration-test/java/org/springframework/session/data/mongo/AbstractITest.java +++ b/src/integrationTest/java/org/mongodb/spring/session/AbstractITest.java @@ -1,11 +1,12 @@ /* + * Copyright 2025-present MongoDB, Inc. * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,13 +15,11 @@ * limitations under the License. */ -package org.springframework.session.data.mongo; +package org.mongodb.spring.session; import java.util.UUID; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; @@ -37,27 +36,26 @@ @WebAppConfiguration public abstract class AbstractITest { - protected SecurityContext context; - - protected SecurityContext changedContext; + protected SecurityContext context; - // @Autowired(required = false) - // protected SessionEventRegistry registry; + protected SecurityContext changedContext; - @BeforeEach - void setup() { + // @Autowired(required = false) + // protected SessionEventRegistry registry; - // if (this.registry != null) { - // this.registry.clear(); - // } + @BeforeEach + void setup() { - this.context = SecurityContextHolder.createEmptyContext(); - this.context.setAuthentication(new UsernamePasswordAuthenticationToken("username-" + UUID.randomUUID(), "na", - AuthorityUtils.createAuthorityList("ROLE_USER"))); + // if (this.registry != null) { + // this.registry.clear(); + // } - this.changedContext = SecurityContextHolder.createEmptyContext(); - this.changedContext.setAuthentication(new UsernamePasswordAuthenticationToken( - "changedContext-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER"))); - } + this.context = SecurityContextHolder.createEmptyContext(); + this.context.setAuthentication(new UsernamePasswordAuthenticationToken( + "username-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER"))); + this.changedContext = SecurityContextHolder.createEmptyContext(); + this.changedContext.setAuthentication(new UsernamePasswordAuthenticationToken( + "changedContext-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER"))); + } } diff --git a/src/integrationTest/java/org/mongodb/spring/session/AbstractMongoRepositoryITest.java b/src/integrationTest/java/org/mongodb/spring/session/AbstractMongoRepositoryITest.java new file mode 100644 index 0000000..01063f2 --- /dev/null +++ b/src/integrationTest/java/org/mongodb/spring/session/AbstractMongoRepositoryITest.java @@ -0,0 +1,397 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mongodb.spring.session.MongoSessionUtils.DEFAULT_DATABASE_NAME; +import static org.mongodb.spring.session.MongoSessionUtils.getConnectionString; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.Session; + +/** + * Abstract base class for {@link MongoIndexedSessionRepository} tests. + * + * @author Jakub Kubrynski + * @author Vedran Pavic + * @author Greg Turnquist + */ +public abstract class AbstractMongoRepositoryITest extends AbstractITest { + + protected static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + + protected static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; + + @Autowired + protected MongoIndexedSessionRepository repository; + + @Test + void saves() { + + String username = "saves-" + System.currentTimeMillis(); + + MongoSession toSave = this.repository.createSession(); + String expectedAttributeName = "a"; + String expectedAttributeValue = "b"; + toSave.setAttribute(expectedAttributeName, expectedAttributeValue); + Authentication toSaveToken = new UsernamePasswordAuthenticationToken( + username, "password", AuthorityUtils.createAuthorityList("ROLE_USER")); + SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext(); + toSaveContext.setAuthentication(toSaveToken); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, toSaveContext); + toSave.setAttribute(INDEX_NAME, username); + + this.repository.save(toSave); + + Session session = this.repository.findById(toSave.getId()); + + assertThat(session.getId()).isEqualTo(toSave.getId()); + assertThat(session.getAttributeNames()).isEqualTo(toSave.getAttributeNames()); + assertThat(session.getAttribute(expectedAttributeName)) + .isEqualTo(toSave.getAttribute(expectedAttributeName)); + + this.repository.deleteById(toSave.getId()); + + String id = toSave.getId(); + assertThat(this.repository.findById(id)).isNull(); + } + + @Test + void putAllOnSingleAttrDoesNotRemoveOld() { + + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute("a", "b"); + + this.repository.save(toSave); + toSave = this.repository.findById(toSave.getId()); + + toSave.setAttribute("1", "2"); + + this.repository.save(toSave); + toSave = this.repository.findById(toSave.getId()); + + Session session = this.repository.findById(toSave.getId()); + assertThat(session.getAttributeNames().size()).isEqualTo(2); + assertThat(session.getAttribute("a")).isEqualTo("b"); + assertThat(session.getAttribute("1")).isEqualTo("2"); + + this.repository.deleteById(toSave.getId()); + } + + @Test + void findByPrincipalName() throws Exception { + + String principalName = "findByPrincipalName" + UUID.randomUUID(); + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(INDEX_NAME, principalName); + + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + + this.repository.deleteById(toSave.getId()); + + findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName); + + assertThat(findByPrincipalName).hasSize(0); + assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId()); + } + + @Test + void nonExistentSessionShouldNotBreakMongo() { + this.repository.deleteById("doesn't exist"); + } + + @Test + void findByPrincipalNameNoPrincipalNameChange() throws Exception { + + String principalName = "findByPrincipalNameNoPrincipalNameChange" + UUID.randomUUID(); + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(INDEX_NAME, principalName); + + this.repository.save(toSave); + + toSave.setAttribute("other", "value"); + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + } + + @Test + void findByPrincipalNameNoPrincipalNameChangeReload() throws Exception { + + String principalName = "findByPrincipalNameNoPrincipalNameChangeReload" + UUID.randomUUID(); + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(INDEX_NAME, principalName); + + this.repository.save(toSave); + + toSave = this.repository.findById(toSave.getId()); + + toSave.setAttribute("other", "value"); + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + } + + @Test + void findByDeletedPrincipalName() throws Exception { + + String principalName = "findByDeletedPrincipalName" + UUID.randomUUID(); + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(INDEX_NAME, principalName); + + this.repository.save(toSave); + + toSave.setAttribute(INDEX_NAME, null); + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName); + + assertThat(findByPrincipalName).isEmpty(); + } + + @Test + void findByChangedPrincipalName() throws Exception { + + String principalName = "findByChangedPrincipalName" + UUID.randomUUID(); + String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID(); + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(INDEX_NAME, principalName); + + this.repository.save(toSave); + + toSave.setAttribute(INDEX_NAME, principalNameChanged); + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName); + assertThat(findByPrincipalName).isEmpty(); + + findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + } + + @Test + void findByDeletedPrincipalNameReload() throws Exception { + + String principalName = "findByDeletedPrincipalName" + UUID.randomUUID(); + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(INDEX_NAME, principalName); + + this.repository.save(toSave); + + MongoSession getSession = this.repository.findById(toSave.getId()); + getSession.setAttribute(INDEX_NAME, null); + this.repository.save(getSession); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName); + + assertThat(findByPrincipalName).isEmpty(); + } + + @Test + void findByChangedPrincipalNameReload() throws Exception { + + String principalName = "findByChangedPrincipalName" + UUID.randomUUID(); + String principalNameChanged = "findByChangedPrincipalName" + UUID.randomUUID(); + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(INDEX_NAME, principalName); + + this.repository.save(toSave); + + MongoSession getSession = this.repository.findById(toSave.getId()); + + getSession.setAttribute(INDEX_NAME, principalNameChanged); + this.repository.save(getSession); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName); + assertThat(findByPrincipalName).isEmpty(); + + findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalNameChanged); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + } + + @Test + void findBySecurityPrincipalName() throws Exception { + + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); + + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + + this.repository.deleteById(toSave.getId()); + + findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); + + assertThat(findByPrincipalName).hasSize(0); + assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId()); + } + + @Test + void findByPrincipalNameNoSecurityPrincipalNameChange() throws Exception { + + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); + + this.repository.save(toSave); + + toSave.setAttribute("other", "value"); + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + } + + @Test + void findByDeletedSecurityPrincipalName() throws Exception { + + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); + + this.repository.save(toSave); + + toSave.setAttribute(SPRING_SECURITY_CONTEXT, null); + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); + + assertThat(findByPrincipalName).isEmpty(); + } + + @Test + void findByChangedSecurityPrincipalName() throws Exception { + + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); + + this.repository.save(toSave); + + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext); + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); + assertThat(findByPrincipalName).isEmpty(); + + findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName()); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + } + + @Test + void findByChangedSecurityPrincipalNameReload() throws Exception { + + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); + + this.repository.save(toSave); + + MongoSession getSession = this.repository.findById(toSave.getId()); + + getSession.setAttribute(SPRING_SECURITY_CONTEXT, this.changedContext); + this.repository.save(getSession); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); + assertThat(findByPrincipalName).isEmpty(); + + findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName()); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + } + + @Test + void loadExpiredSession() throws Exception { + + // given + MongoSession expiredSession = this.repository.createSession(); + Instant thirtyOneMinutesAgo = + Instant.ofEpochMilli(System.currentTimeMillis()).minus(Duration.ofMinutes(31)); + expiredSession.setLastAccessedTime(thirtyOneMinutesAgo); + this.repository.save(expiredSession); + + // then + MongoSession expiredSessionFromDb = this.repository.findById(expiredSession.getId()); + assertThat(expiredSessionFromDb).isNull(); + } + + protected String getSecurityName() { + return this.context.getAuthentication().getName(); + } + + protected String getChangedSecurityName() { + return this.changedContext.getAuthentication().getName(); + } + + protected static class BaseConfig { + + @Bean + public MongoOperations mongoOperations() { + MongoClient mongo = MongoClients.create(getConnectionString()); + return new MongoTemplate(mongo, DEFAULT_DATABASE_NAME); + } + } +} diff --git a/src/integrationTest/java/org/mongodb/spring/session/MongoDbDeleteJacksonSessionVerificationTest.java b/src/integrationTest/java/org/mongodb/spring/session/MongoDbDeleteJacksonSessionVerificationTest.java new file mode 100644 index 0000000..b4fc809 --- /dev/null +++ b/src/integrationTest/java/org/mongodb/spring/session/MongoDbDeleteJacksonSessionVerificationTest.java @@ -0,0 +1,208 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import static org.mongodb.spring.session.MongoSessionUtils.DEFAULT_DATABASE_NAME; +import static org.mongodb.spring.session.MongoSessionUtils.getConnectionString; + +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import java.net.URI; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mongodb.spring.session.config.annotation.web.reactive.EnableMongoWebSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.reactive.server.FluxExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.BodyInserters; +import reactor.test.StepVerifier; + +/** @author Boris Finkelshteyn */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +class MongoDbDeleteJacksonSessionVerificationTest { + + @Autowired + ApplicationContext ctx; + + WebTestClient client; + + @BeforeEach + void setUp() { + this.client = WebTestClient.bindToApplicationContext(this.ctx).build(); + } + + @Test + void logoutShouldDeleteOldSessionFromMongoDB() { + + // 1. Login and capture the SESSION cookie value. + // @formatter:off + FluxExchangeResult loginResult = this.client + .post() + .uri("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData("username", "admin").with("password", "password")) + .exchange() + .returnResult(String.class); + // @formatter:on + + AssertionsForClassTypes.assertThat(loginResult.getResponseHeaders().getLocation()) + .isEqualTo(URI.create("/")); + + String originalSessionId = + loginResult.getResponseCookies().getFirst("SESSION").getValue(); + + // 2. Fetch a protected resource using the SESSION cookie. + // @formatter:off + this.client + .get() + .uri("/hello") + .cookie("SESSION", originalSessionId) + .exchange() + .expectStatus() + .isOk() + .returnResult(String.class) + .getResponseBody() + .as(StepVerifier::create) + .expectNext("HelloWorld") + .verifyComplete(); + // @formatter:on + + // 3. Logout using the SESSION cookie, and capture the new SESSION cookie. + // @formatter:off + String newSessionId = this.client + .post() + .uri("/logout") + .cookie("SESSION", originalSessionId) + .exchange() + .expectStatus() + .isFound() + .returnResult(String.class) + .getResponseCookies() + .getFirst("SESSION") + .getValue(); + // @formatter:on + + AssertionsForClassTypes.assertThat(newSessionId).isNotEqualTo(originalSessionId); + + // 4. Verify the new SESSION cookie is not yet authorized. + // @formatter:off + this.client + .get() + .uri("/hello") + .cookie("SESSION", newSessionId) + .exchange() + .expectStatus() + .isFound() + .expectHeader() + .value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value) + .isEqualTo("/login")); + // @formatter:on + + // 5. Verify the original SESSION cookie no longer works. + // @formatter:off + this.client + .get() + .uri("/hello") + .cookie("SESSION", originalSessionId) + .exchange() + .expectStatus() + .isFound() + .expectHeader() + .value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value) + .isEqualTo("/login")); + // @formatter:on + } + + @RestController + static class TestController { + + @GetMapping("/hello") + ResponseEntity hello() { + return ResponseEntity.ok("HelloWorld"); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableWebFluxSecurity + static class SecurityConfig { + + @Bean + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + // @formatter:off + return http.logout(Customizer.withDefaults()) + .formLogin(Customizer.withDefaults()) + .csrf((csrf) -> csrf.disable()) + .authorizeExchange((ae) -> ae.anyExchange().authenticated()) + .build(); + // @formatter:on + } + + @Bean + MapReactiveUserDetailsService userDetailsService() { + // @formatter:off + return new MapReactiveUserDetailsService(User.withUsername("admin") + .password("{noop}password") + .roles("USER,ADMIN") + .build()); + // @formatter:on + } + + @Bean + AbstractMongoSessionConverter mongoSessionConverter() { + return new JacksonMongoSessionConverter(); + } + } + + @Configuration + @EnableWebFlux + @EnableMongoWebSession + static class Config { + @Bean + ReactiveMongoOperations mongoOperations() { + MongoClient mongo = MongoClients.create(getConnectionString()); + return new ReactiveMongoTemplate(mongo, DEFAULT_DATABASE_NAME); + } + + @Bean + TestController controller() { + return new TestController(); + } + } +} diff --git a/src/integrationTest/java/org/mongodb/spring/session/MongoDbLogoutVerificationTest.java b/src/integrationTest/java/org/mongodb/spring/session/MongoDbLogoutVerificationTest.java new file mode 100644 index 0000000..a0d61ba --- /dev/null +++ b/src/integrationTest/java/org/mongodb/spring/session/MongoDbLogoutVerificationTest.java @@ -0,0 +1,202 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import static org.mongodb.spring.session.MongoSessionUtils.DEFAULT_DATABASE_NAME; +import static org.mongodb.spring.session.MongoSessionUtils.getConnectionString; + +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import java.net.URI; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mongodb.spring.session.config.annotation.web.reactive.EnableMongoWebSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.reactive.server.FluxExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.BodyInserters; +import reactor.test.StepVerifier; + +/** @author Greg Turnquist */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +class MongoDbLogoutVerificationTest { + + @Autowired + ApplicationContext ctx; + + WebTestClient client; + + @BeforeEach + void setUp() { + this.client = WebTestClient.bindToApplicationContext(this.ctx).build(); + } + + @Test + void logoutShouldDeleteOldSessionFromMongoDB() { + + // 1. Login and capture the SESSION cookie value. + + FluxExchangeResult loginResult = this.client + .post() + .uri("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) // + .body( + BodyInserters // + .fromFormData("username", "admin") // + .with("password", "password")) // + .exchange() // + .returnResult(String.class); + + AssertionsForClassTypes.assertThat(loginResult.getResponseHeaders().getLocation()) + .isEqualTo(URI.create("/")); + + String originalSessionId = + loginResult.getResponseCookies().getFirst("SESSION").getValue(); + + // 2. Fetch a protected resource using the SESSION cookie. + + this.client + .get() + .uri("/hello") // + .cookie("SESSION", originalSessionId) // + .exchange() // + .expectStatus() + .isOk() // + .returnResult(String.class) + .getResponseBody() // + .as(StepVerifier::create) // + .expectNext("HelloWorld") // + .verifyComplete(); + + // 3. Logout using the SESSION cookie, and capture the new SESSION cookie. + + String newSessionId = this.client + .post() + .uri("/logout") // + .cookie("SESSION", originalSessionId) // + .exchange() // + .expectStatus() + .isFound() // + .returnResult(String.class) + .getResponseCookies() + .getFirst("SESSION") + .getValue(); + + AssertionsForClassTypes.assertThat(newSessionId).isNotEqualTo(originalSessionId); + + // 4. Verify the new SESSION cookie is not yet authorized. + + this.client + .get() + .uri("/hello") // + .cookie("SESSION", newSessionId) // + .exchange() // + .expectStatus() + .isFound() // + .expectHeader() + .value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value) + .isEqualTo("/login")); + + // 5. Verify the original SESSION cookie no longer works. + + this.client + .get() + .uri("/hello") // + .cookie("SESSION", originalSessionId) // + .exchange() // + .expectStatus() + .isFound() // + .expectHeader() + .value(HttpHeaders.LOCATION, (value) -> AssertionsForClassTypes.assertThat(value) + .isEqualTo("/login")); + } + + @RestController + static class TestController { + + @GetMapping("/hello") + ResponseEntity hello() { + return ResponseEntity.ok("HelloWorld"); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableWebFluxSecurity + static class SecurityConfig { + + @Bean + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + // @formatter:off + return http.logout(Customizer.withDefaults()) + .formLogin(Customizer.withDefaults()) + .csrf((csrf) -> csrf.disable()) + .authorizeExchange((ae) -> ae.anyExchange().authenticated()) + .build(); + // @formatter:on + } + + @Bean + MapReactiveUserDetailsService userDetailsService() { + // @formatter:off + return new MapReactiveUserDetailsService(User.withUsername("admin") + .password("{noop}password") + .roles("USER,ADMIN") + .build()); + // @formatter:on + } + } + + @Configuration + @EnableWebFlux + @EnableMongoWebSession + static class Config { + + @Bean + ReactiveMongoOperations mongoOperations() { + MongoClient mongo = MongoClients.create(getConnectionString()); + return new ReactiveMongoTemplate(mongo, DEFAULT_DATABASE_NAME); + } + + @Bean + TestController controller() { + return new TestController(); + } + } +} diff --git a/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJacksonITest.java b/src/integrationTest/java/org/mongodb/spring/session/MongoRepositoryJacksonITest.java similarity index 52% rename from src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJacksonITest.java rename to src/integrationTest/java/org/mongodb/spring/session/MongoRepositoryJacksonITest.java index 417341b..3eb01c4 100644 --- a/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJacksonITest.java +++ b/src/integrationTest/java/org/mongodb/spring/session/MongoRepositoryJacksonITest.java @@ -1,11 +1,12 @@ /* + * Copyright 2025-present MongoDB, Inc. * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,25 +15,22 @@ * limitations under the License. */ -package org.springframework.session.data.mongo; +package org.mongodb.spring.session; + +import static org.assertj.core.api.Assertions.assertThat; import java.util.Collections; import java.util.Map; import java.util.UUID; - import org.junit.jupiter.api.Test; - +import org.mongodb.spring.session.config.annotation.web.http.EnableMongoHttpSession; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.geo.GeoModule; -import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession; import org.springframework.test.context.ContextConfiguration; -import static org.assertj.core.api.Assertions.assertThat; - /** - * Integration tests for - * {@link org.springframework.session.data.mongo.MongoIndexedSessionRepository} that use + * Integration tests for {@link org.mongodb.spring.session.MongoIndexedSessionRepository} that use * {@link JacksonMongoSessionConverter} based session serialization. * * @author Jakub Kubrynski @@ -42,32 +40,31 @@ @ContextConfiguration class MongoRepositoryJacksonITest extends AbstractMongoRepositoryITest { - @Test - void findByCustomIndex() throws Exception { - - MongoSession toSave = this.repository.createSession(); - String cartId = "cart-" + UUID.randomUUID(); - toSave.setAttribute("cartId", cartId); + @Test + void findByCustomIndex() throws Exception { - this.repository.save(toSave); + MongoSession toSave = this.repository.createSession(); + String cartId = "cart-" + UUID.randomUUID(); + toSave.setAttribute("cartId", cartId); - Map findByCartId = this.repository.findByIndexNameAndIndexValue("cartId", cartId); + this.repository.save(toSave); - assertThat(findByCartId).hasSize(1); - assertThat(findByCartId.keySet()).containsOnly(toSave.getId()); - } + Map findByCartId = this.repository.findByIndexNameAndIndexValue("cartId", cartId); - // tag::sample[] - @Configuration - @EnableMongoHttpSession - static class Config extends BaseConfig { + assertThat(findByCartId).hasSize(1); + assertThat(findByCartId.keySet()).containsOnly(toSave.getId()); + } - @Bean - AbstractMongoSessionConverter mongoSessionConverter() { - return new JacksonMongoSessionConverter(Collections.singletonList(new GeoModule())); - } + // tag::sample[] + @Configuration + @EnableMongoHttpSession + static class Config extends BaseConfig { - } - // end::sample[] + @Bean + AbstractMongoSessionConverter mongoSessionConverter() { + return new JacksonMongoSessionConverter(Collections.singletonList(new GeoModule())); + } + } + // end::sample[] } diff --git a/src/integrationTest/java/org/mongodb/spring/session/MongoRepositoryJdkSerializationITest.java b/src/integrationTest/java/org/mongodb/spring/session/MongoRepositoryJdkSerializationITest.java new file mode 100644 index 0000000..85d8943 --- /dev/null +++ b/src/integrationTest/java/org/mongodb/spring/session/MongoRepositoryJdkSerializationITest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mongodb.spring.session.config.annotation.web.http.EnableMongoHttpSession; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +/** + * Integration tests for {@link org.mongodb.spring.session.MongoIndexedSessionRepository} that use + * {@link JdkMongoSessionConverter} based session serialization. + * + * @author Jakub Kubrynski + * @author Vedran Pavic + * @author Greg Turnquist + */ +@ContextConfiguration +class MongoRepositoryJdkSerializationITest extends AbstractMongoRepositoryITest { + + @Test + void findByDeletedSecurityPrincipalNameReload() throws Exception { + + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); + + this.repository.save(toSave); + + MongoSession getSession = this.repository.findById(toSave.getId()); + getSession.setAttribute(INDEX_NAME, null); + this.repository.save(getSession); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName()); + + assertThat(findByPrincipalName).isEmpty(); + } + + @Test + void findByPrincipalNameNoSecurityPrincipalNameChangeReload() throws Exception { + + MongoSession toSave = this.repository.createSession(); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); + + this.repository.save(toSave); + + toSave = this.repository.findById(toSave.getId()); + + toSave.setAttribute("other", "value"); + this.repository.save(toSave); + + Map findByPrincipalName = + this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + } + + // tag::sample[] + @Configuration + @EnableMongoHttpSession + static class Config extends BaseConfig { + + @Bean + AbstractMongoSessionConverter mongoSessionConverter() { + return new JdkMongoSessionConverter(Duration.ofMinutes(30)); + } + } + // end::sample[] + +} diff --git a/src/main/java/org/mongodb/spring/session/AbstractMongoSessionConverter.java b/src/main/java/org/mongodb/spring/session/AbstractMongoSessionConverter.java new file mode 100644 index 0000000..811ff6a --- /dev/null +++ b/src/main/java/org/mongodb/spring/session/AbstractMongoSessionConverter.java @@ -0,0 +1,139 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import com.mongodb.DBObject; +import java.util.Collections; +import java.util.Set; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bson.Document; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.index.Index; +import org.springframework.data.mongodb.core.index.IndexInfo; +import org.springframework.data.mongodb.core.index.IndexOperations; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.session.DelegatingIndexResolver; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.IndexResolver; +import org.springframework.session.PrincipalNameIndexResolver; +import org.springframework.session.Session; +import org.springframework.util.Assert; + +/** + * Base class for serializing and deserializing session objects. To create custom serializer you have to implement this + * interface and simply register your class as a bean. + * + * @author Jakub Kubrynski + * @author Greg Turnquist + * @since 1.2 + */ +public abstract class AbstractMongoSessionConverter implements GenericConverter { + + static final String EXPIRE_AT_FIELD_NAME = "expireAt"; + + private static final Log LOG = LogFactory.getLog(AbstractMongoSessionConverter.class); + + private IndexResolver indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>()); + + /** + * Returns query to be executed to return sessions based on a particular index. + * + * @param indexName name of the index + * @param indexValue value to query against + * @return built query or null if indexName is not supported + */ + @Nullable protected abstract Query getQueryForIndex(String indexName, Object indexValue); + + /** + * Method ensures that there is a TTL index on {@literal expireAt} field. It's has {@literal expireAfterSeconds} set + * to zero seconds, so the expiration time is controlled by the application. It can be extended in custom converters + * when there is a need for creating additional custom indexes. + * + * @param sessionCollectionIndexes {@link IndexOperations} to use + */ + protected void ensureIndexes(IndexOperations sessionCollectionIndexes) { + + for (IndexInfo info : sessionCollectionIndexes.getIndexInfo()) { + if (EXPIRE_AT_FIELD_NAME.equals(info.getName())) { + LOG.debug("TTL index on field " + EXPIRE_AT_FIELD_NAME + " already exists"); + return; + } + } + + LOG.info("Creating TTL index on field " + EXPIRE_AT_FIELD_NAME); + + try { + sessionCollectionIndexes.createIndex(new Index(EXPIRE_AT_FIELD_NAME, Sort.Direction.ASC) + .named(EXPIRE_AT_FIELD_NAME) + .expire(0)); + } catch (Exception e) { + // Handle the case where the index already exists (error code 85 for MongoCommandException) + if (e instanceof com.mongodb.MongoCommandException + && ((com.mongodb.MongoCommandException) e).getErrorCode() == 85) { + LOG.debug("TTL index on field " + EXPIRE_AT_FIELD_NAME + " already exists (caught during creation)"); + } else if (e instanceof org.springframework.dao.DuplicateKeyException) { + LOG.debug("TTL index on field " + EXPIRE_AT_FIELD_NAME + " already exists (DuplicateKeyException)"); + } else { + // Unexpected error, rethrow + throw e; + } + } + } + + @Nullable protected String extractPrincipal(MongoSession expiringSession) { + + return this.indexResolver + .resolveIndexesFor(expiringSession) + .get(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME); + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(DBObject.class, MongoSession.class)); + } + + @SuppressWarnings("unchecked") + @Override + @Nullable public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (source == null) { + return null; + } + + if (DBObject.class.isAssignableFrom(sourceType.getType())) { + return convert(new Document(((DBObject) source).toMap())); + } else if (Document.class.isAssignableFrom(sourceType.getType())) { + return convert((Document) source); + } else { + return convert((MongoSession) source); + } + } + + protected abstract DBObject convert(MongoSession session); + + @Nullable protected abstract MongoSession convert(Document sessionWrapper); + + public void setIndexResolver(IndexResolver indexResolver) { + Assert.notNull(indexResolver, "indexResolver must not be null"); + this.indexResolver = indexResolver; + } +} diff --git a/src/main/java/org/mongodb/spring/session/JacksonMongoSessionConverter.java b/src/main/java/org/mongodb/spring/session/JacksonMongoSessionConverter.java new file mode 100644 index 0000000..c605bf5 --- /dev/null +++ b/src/main/java/org/mongodb/spring/session/JacksonMongoSessionConverter.java @@ -0,0 +1,180 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bson.Document; +import org.bson.json.JsonMode; +import org.bson.json.JsonWriterSettings; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.util.Assert; + +/** + * {@code AbstractMongoSessionConverter} implementation using Jackson. + * + * @author Jakub Kubrynski + * @author Greg Turnquist + * @author Michael Ruf + * @since 1.2 + */ +public class JacksonMongoSessionConverter extends AbstractMongoSessionConverter { + + private static final Log LOG = LogFactory.getLog(JacksonMongoSessionConverter.class); + + private static final String ATTRS_FIELD_NAME = "attrs."; + + private static final String PRINCIPAL_FIELD_NAME = "principal"; + + private static final String EXPIRE_AT_FIELD_NAME = "expireAt"; + + private final ObjectMapper objectMapper; + + public JacksonMongoSessionConverter() { + this(Collections.emptyList()); + } + + public JacksonMongoSessionConverter(Iterable modules) { + + this.objectMapper = buildObjectMapper(); + this.objectMapper.registerModules(modules); + } + + public JacksonMongoSessionConverter(ObjectMapper objectMapper) { + + Assert.notNull(objectMapper, "ObjectMapper can NOT be null!"); + this.objectMapper = objectMapper; + } + + @Override + @Nullable protected Query getQueryForIndex(String indexName, Object indexValue) { + + if (FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) { + return Query.query(Criteria.where(PRINCIPAL_FIELD_NAME).is(indexValue)); + } else { + return Query.query(Criteria.where(ATTRS_FIELD_NAME + MongoSession.coverDot(indexName)) + .is(indexValue)); + } + } + + private ObjectMapper buildObjectMapper() { + + ObjectMapper objectMapper = new ObjectMapper(); + + // serialize fields instead of properties + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + // ignore unresolved fields (mostly 'principal') + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + objectMapper.setPropertyNamingStrategy(new MongoIdNamingStrategy()); + + objectMapper.registerModules( + SecurityJackson2Modules.getModules(getClass().getClassLoader())); + objectMapper.addMixIn(MongoSession.class, MongoSessionMixin.class); + objectMapper.addMixIn(HashMap.class, HashMapMixin.class); + + return objectMapper; + } + + @Override + protected DBObject convert(MongoSession source) { + + try { + DBObject dbSession = BasicDBObject.parse(this.objectMapper.writeValueAsString(source)); + + // Override default serialization with proper values. + dbSession.put(PRINCIPAL_FIELD_NAME, extractPrincipal(source)); + dbSession.put(EXPIRE_AT_FIELD_NAME, source.getExpireAt()); + return dbSession; + } catch (JsonProcessingException ex) { + throw new IllegalStateException("Cannot convert MongoExpiringSession", ex); + } + } + + @Override + @Nullable protected MongoSession convert(Document source) { + + Date expireAt = (Date) source.remove(EXPIRE_AT_FIELD_NAME); + source.remove("originalSessionId"); + String json = source.toJson( + JsonWriterSettings.builder().outputMode(JsonMode.RELAXED).build()); + + try { + MongoSession mongoSession = this.objectMapper.readValue(json, MongoSession.class); + mongoSession.setExpireAt(expireAt); + return mongoSession; + } catch (IOException ex) { + LOG.error("Error during Mongo Session deserialization", ex); + return null; + } + } + + /** Used to whitelist {@link MongoSession} for {@link SecurityJackson2Modules}. */ + @SuppressWarnings("unused") + private static class MongoSessionMixin { + + @JsonCreator + MongoSessionMixin( + @JsonProperty("_id") String id, @JsonProperty("intervalSeconds") long maxInactiveIntervalInSeconds) {} + } + + /** Used to whitelist {@link HashMap} for {@link SecurityJackson2Modules}. */ + private static class HashMapMixin { + + // Nothing special + + } + + private static class MongoIdNamingStrategy extends PropertyNamingStrategies.NamingBase { + private static final long serialVersionUID = 2L; + + @Override + public String translate(String propertyName) { + + switch (propertyName) { + case "id": + return "_id"; + case "_id": + return "id"; + default: + return propertyName; + } + } + } +} diff --git a/src/main/java/org/mongodb/spring/session/JdkMongoSessionConverter.java b/src/main/java/org/mongodb/spring/session/JdkMongoSessionConverter.java new file mode 100644 index 0000000..4692cd5 --- /dev/null +++ b/src/main/java/org/mongodb/spring/session/JdkMongoSessionConverter.java @@ -0,0 +1,175 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import static java.lang.String.format; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import org.bson.Document; +import org.bson.types.Binary; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.serializer.support.DeserializingConverter; +import org.springframework.core.serializer.support.SerializingConverter; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.Session; +import org.springframework.util.Assert; + +/** + * {@code AbstractMongoSessionConverter} implementation using standard Java serialization. + * + * @author Jakub Kubrynski + * @author Rob Winch + * @author Greg Turnquist + * @since 1.2 + */ +public class JdkMongoSessionConverter extends AbstractMongoSessionConverter { + + private static final String ID = "_id"; + + private static final String CREATION_TIME = "created"; + + private static final String LAST_ACCESSED_TIME = "accessed"; + + private static final String MAX_INTERVAL = "interval"; + + private static final String ATTRIBUTES = "attr"; + + private static final String PRINCIPAL_FIELD_NAME = "principal"; + + private final Converter serializer; + + private final Converter deserializer; + + private Duration maxInactiveInterval; + + public JdkMongoSessionConverter(Duration maxInactiveInterval) { + this(new SerializingConverter(), new DeserializingConverter(), maxInactiveInterval); + } + + public JdkMongoSessionConverter( + Converter serializer, + Converter deserializer, + Duration maxInactiveInterval) { + + Assert.notNull(serializer, "serializer cannot be null"); + Assert.notNull(deserializer, "deserializer cannot be null"); + Assert.notNull(maxInactiveInterval, "maxInactiveInterval cannot be null"); + + this.serializer = serializer; + this.deserializer = deserializer; + this.maxInactiveInterval = maxInactiveInterval; + } + + @Override + @Nullable public Query getQueryForIndex(String indexName, Object indexValue) { + + if (FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) { + return Query.query(Criteria.where(PRINCIPAL_FIELD_NAME).is(indexValue)); + } else { + return null; + } + } + + @Override + protected DBObject convert(MongoSession session) { + + BasicDBObject basicDBObject = new BasicDBObject(); + + basicDBObject.put(ID, session.getId()); + basicDBObject.put(CREATION_TIME, session.getCreationTime()); + basicDBObject.put(LAST_ACCESSED_TIME, session.getLastAccessedTime()); + basicDBObject.put(MAX_INTERVAL, session.getMaxInactiveInterval()); + basicDBObject.put(PRINCIPAL_FIELD_NAME, extractPrincipal(session)); + basicDBObject.put(EXPIRE_AT_FIELD_NAME, session.getExpireAt()); + basicDBObject.put(ATTRIBUTES, serializeAttributes(session)); + + return basicDBObject; + } + + @Override + @SuppressWarnings("NullAway") + protected MongoSession convert(Document sessionWrapper) { + + Object maxInterval = sessionWrapper.getOrDefault(MAX_INTERVAL, this.maxInactiveInterval); + + Duration maxIntervalDuration = + (maxInterval instanceof Duration) ? (Duration) maxInterval : Duration.parse(maxInterval.toString()); + + MongoSession session = new MongoSession(sessionWrapper.getString(ID), maxIntervalDuration.toSeconds()); + + Object creationTime = sessionWrapper.get(CREATION_TIME); + if (creationTime instanceof Instant) { + session.setCreationTime(((Instant) creationTime).toEpochMilli()); + } else if (creationTime instanceof Date) { + session.setCreationTime(((Date) creationTime).toInstant().toEpochMilli()); + } + + Object lastAccessedTime = sessionWrapper.get(LAST_ACCESSED_TIME); + if (lastAccessedTime instanceof Instant) { + session.setLastAccessedTime((Instant) lastAccessedTime); + } else if (lastAccessedTime instanceof Date) { + session.setLastAccessedTime(((Date) lastAccessedTime).toInstant()); + } + + Object expires = sessionWrapper.get(EXPIRE_AT_FIELD_NAME); + Assert.notNull(expires, () -> format("%s missing from session.", EXPIRE_AT_FIELD_NAME)); + session.setExpireAt((Date) expires); + + deserializeAttributes(sessionWrapper, session); + + return session; + } + + @Nullable private byte[] serializeAttributes(Session session) { + + Map attributes = new HashMap<>(); + + for (String attrName : session.getAttributeNames()) { + attributes.put(attrName, session.getAttribute(attrName)); + } + + return this.serializer.convert(attributes); + } + + @SuppressWarnings("unchecked") + private void deserializeAttributes(Document sessionWrapper, Session session) { + + Object sessionAttributes = sessionWrapper.get(ATTRIBUTES); + + byte[] attributesBytes = ((sessionAttributes instanceof Binary) + ? ((Binary) sessionAttributes).getData() + : (byte[]) sessionAttributes); + + Map attributes = (Map) this.deserializer.convert(attributesBytes); + + if (attributes != null) { + for (Map.Entry entry : attributes.entrySet()) { + session.setAttribute(entry.getKey(), entry.getValue()); + } + } + } +} diff --git a/src/main/java/org/mongodb/spring/session/MongoIndexedSessionRepository.java b/src/main/java/org/mongodb/spring/session/MongoIndexedSessionRepository.java new file mode 100644 index 0000000..d188864 --- /dev/null +++ b/src/main/java/org/mongodb/spring/session/MongoIndexedSessionRepository.java @@ -0,0 +1,233 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import com.mongodb.DBObject; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bson.Document; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.index.IndexOperations; +import org.springframework.lang.Nullable; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.MapSession; +import org.springframework.session.SessionIdGenerator; +import org.springframework.session.UuidSessionIdGenerator; +import org.springframework.session.events.SessionCreatedEvent; +import org.springframework.session.events.SessionDeletedEvent; +import org.springframework.session.events.SessionExpiredEvent; +import org.springframework.util.Assert; + +/** + * Session repository implementation which stores sessions in Mongo. Uses {@link AbstractMongoSessionConverter} to + * transform session objects from/to native Mongo representation ({@code DBObject}). Repository is also responsible for + * removing expired sessions from database. Cleanup is done every minute. + * + * @author Jakub Kubrynski + * @author Greg Turnquist + * @author Vedran Pavic + * @since 2.2.0 + */ +public class MongoIndexedSessionRepository + implements FindByIndexNameSessionRepository, ApplicationEventPublisherAware, InitializingBean { + + /** + * The default time period in seconds in which a session will expire. + * + * @deprecated since 3.0.0 in favor of {@link MapSession#DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS} + */ + @Deprecated + public static final int DEFAULT_INACTIVE_INTERVAL = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; + + /** the default collection name for storing session. */ + public static final String DEFAULT_COLLECTION_NAME = "sessions"; + + private static final Log logger = LogFactory.getLog(MongoIndexedSessionRepository.class); + + private final MongoOperations mongoOperations; + + private Duration defaultMaxInactiveInterval = Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); + + private String collectionName = DEFAULT_COLLECTION_NAME; + + private AbstractMongoSessionConverter mongoSessionConverter = + new JdkMongoSessionConverter(this.defaultMaxInactiveInterval); + + @Nullable private ApplicationEventPublisher eventPublisher; + + @Nullable private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance(); + + public MongoIndexedSessionRepository(MongoOperations mongoOperations) { + this.mongoOperations = mongoOperations; + } + + @Override + @SuppressWarnings("NullAway") + public MongoSession createSession() { + Assert.notNull(this.sessionIdGenerator, "sessionIdGenerator not initialized."); + MongoSession session = new MongoSession(this.sessionIdGenerator, this.defaultMaxInactiveInterval.toSeconds()); + + publishEvent(new SessionCreatedEvent(this, session)); + + return session; + } + + @Override + public void save(MongoSession session) { + DBObject dbObject = MongoSessionUtils.convertToDBObject(this.mongoSessionConverter, session); + Assert.notNull(dbObject, "dbObject must not be null"); + this.mongoOperations.save(dbObject, this.collectionName); + } + + @Override + @Nullable @SuppressWarnings("NullAway") + public MongoSession findById(String id) { + + Document sessionWrapper = findSession(id); + + if (sessionWrapper == null) { + return null; + } + + MongoSession session = MongoSessionUtils.convertToSession(this.mongoSessionConverter, sessionWrapper); + + if (session != null) { + if (session.isExpired()) { + publishEvent(new SessionExpiredEvent(this, session)); + deleteById(id); + return null; + } + Assert.notNull(this.sessionIdGenerator, "sessionIdGenerator not initialized."); + session.setSessionIdGenerator(this.sessionIdGenerator); + } + + return session; + } + + /** + * Currently this repository allows only querying against {@code PRINCIPAL_NAME_INDEX_NAME}. + * + * @param indexName the name if the index (i.e. {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME}) + * @param indexValue the value of the index to search for. + * @return sessions map + */ + @Override + @SuppressWarnings("NullAway") + public Map findByIndexNameAndIndexValue(String indexName, String indexValue) { + Assert.notNull(this.sessionIdGenerator, "sessionIdGenerator not initialized."); + return Optional.ofNullable(this.mongoSessionConverter.getQueryForIndex(indexName, indexValue)) + .map((query) -> this.mongoOperations.find(query, Document.class, this.collectionName)) + .orElse(Collections.emptyList()) + .stream() + .map((dbSession) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, dbSession)) + .peek((session) -> session.setSessionIdGenerator(this.sessionIdGenerator)) + .collect(Collectors.toMap(MongoSession::getId, (mapSession) -> mapSession)); + } + + @Override + public void deleteById(String id) { + + Optional.ofNullable(findSession(id)).ifPresent((document) -> { + MongoSession session = MongoSessionUtils.convertToSession(this.mongoSessionConverter, document); + if (session != null) { + publishEvent(new SessionDeletedEvent(this, session)); + } + this.mongoOperations.remove(document, this.collectionName); + }); + } + + @Override + public void afterPropertiesSet() { + + IndexOperations indexOperations = this.mongoOperations.indexOps(this.collectionName); + this.mongoSessionConverter.ensureIndexes(indexOperations); + } + + @Nullable private Document findSession(String id) { + return this.mongoOperations.findById(id, Document.class, this.collectionName); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + private void publishEvent(ApplicationEvent event) { + if (this.eventPublisher == null) { + logger.error("Error publishing " + event + ". No event publisher set."); + } else { + try { + this.eventPublisher.publishEvent(event); + } catch (Throwable ex) { + logger.error("Error publishing " + event + ".", ex); + } + } + } + + /** + * Set the maximum inactive interval in seconds between requests before newly created sessions will be invalidated. + * A negative time indicates that the session will never time out. The default is 30 minutes. + * + * @param defaultMaxInactiveInterval the default maxInactiveInterval + */ + public void setDefaultMaxInactiveInterval(Duration defaultMaxInactiveInterval) { + Assert.notNull(defaultMaxInactiveInterval, "defaultMaxInactiveInterval must not be null"); + this.defaultMaxInactiveInterval = defaultMaxInactiveInterval; + } + + /** + * Set the maximum inactive interval in seconds between requests before newly created sessions will be invalidated. + * A negative time indicates that the session will never time out. The default is 1800 (30 minutes). + * + * @param defaultMaxInactiveInterval the default maxInactiveInterval in seconds + * @deprecated since 3.0.0, in favor of {@link #setDefaultMaxInactiveInterval(Duration)} + */ + @Deprecated(since = "3.0.0") + @SuppressWarnings("InlineMeSuggester") + public void setMaxInactiveIntervalInSeconds(Integer defaultMaxInactiveInterval) { + setDefaultMaxInactiveInterval(Duration.ofSeconds(defaultMaxInactiveInterval)); + } + + public void setCollectionName(final String collectionName) { + this.collectionName = collectionName; + } + + public void setMongoSessionConverter(final AbstractMongoSessionConverter mongoSessionConverter) { + this.mongoSessionConverter = mongoSessionConverter; + } + + /** + * Set the {@link SessionIdGenerator} to use to generate session ids. + * + * @param sessionIdGenerator the {@link SessionIdGenerator} to use + * @since 3.2 + */ + public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { + Assert.notNull(sessionIdGenerator, "sessionIdGenerator cannot be null"); + this.sessionIdGenerator = sessionIdGenerator; + } +} diff --git a/src/main/java/org/mongodb/spring/session/MongoSession.java b/src/main/java/org/mongodb/spring/session/MongoSession.java new file mode 100644 index 0000000..4ce9dbb --- /dev/null +++ b/src/main/java/org/mongodb/spring/session/MongoSession.java @@ -0,0 +1,255 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.lang.Nullable; +import org.springframework.session.MapSession; +import org.springframework.session.Session; +import org.springframework.session.SessionIdGenerator; +import org.springframework.session.UuidSessionIdGenerator; +import org.springframework.util.Assert; + +/** + * Session object providing additional information about the datetime of expiration. + * + * @author Jakub Kubrynski + * @author Greg Turnquist + * @since 1.2 + */ +public final class MongoSession implements Session { + + /** + * Mongo doesn't support {@literal dot} in field names. We replace it with a unicode character from the Private Use + * Area. + * + *

NOTE: This was originally stored in unicode format. Delomboking the code caused it to get converted to another + * encoding, which isn't supported on all systems, so we migrated back to unicode. The same character is being + * represented ensuring binary compatibility. See https://www.compart.com/en/unicode/U+F607 + */ + private static final char DOT_COVER_CHAR = '\uF607'; + + private String id; + + private final String originalSessionId; + + private long createdMillis = System.currentTimeMillis(); + + private long accessedMillis; + + private long intervalSeconds; + + private Date expireAt; + + private final Map attrs = new HashMap<>(); + + private transient SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance(); + + /** + * Constructs a new instance using the provided session id. + * + * @param sessionId the session id to use + * @since 3.2 + */ + public MongoSession(String sessionId) { + this(sessionId, MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); + } + + public MongoSession() { + this(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); + } + + public MongoSession(long maxInactiveIntervalInSeconds) { + this(UuidSessionIdGenerator.getInstance().generate(), maxInactiveIntervalInSeconds); + } + + public MongoSession(String id, long maxInactiveIntervalInSeconds) { + this.id = id; + this.originalSessionId = id; + this.intervalSeconds = maxInactiveIntervalInSeconds; + setLastAccessedTime(Instant.ofEpochMilli(this.createdMillis)); + } + + /** + * Constructs a new instance using the provided {@link SessionIdGenerator}. + * + * @param sessionIdGenerator the {@link SessionIdGenerator} to use + * @since 3.2 + */ + public MongoSession(SessionIdGenerator sessionIdGenerator) { + this(sessionIdGenerator.generate(), MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); + this.sessionIdGenerator = sessionIdGenerator; + } + + /** + * Constructs a new instance using the provided {@link SessionIdGenerator} and max inactive interval. + * + * @param sessionIdGenerator the {@link SessionIdGenerator} to use + * @param maxInactiveIntervalInSeconds the max inactive interval in seconds + * @since 3.2 + */ + MongoSession(SessionIdGenerator sessionIdGenerator, long maxInactiveIntervalInSeconds) { + this(sessionIdGenerator.generate(), maxInactiveIntervalInSeconds); + this.sessionIdGenerator = sessionIdGenerator; + } + + static String coverDot(String attributeName) { + return attributeName.replace('.', DOT_COVER_CHAR); + } + + static String uncoverDot(String attributeName) { + return attributeName.replace(DOT_COVER_CHAR, '.'); + } + + @Override + public String changeSessionId() { + + String changedId = this.sessionIdGenerator.generate(); + this.id = changedId; + return changedId; + } + + @Override + @Nullable @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + public T getAttribute(String attributeName) { + return (T) this.attrs.get(coverDot(attributeName)); + } + + @Override + public Set getAttributeNames() { + return this.attrs.keySet().stream().map(MongoSession::uncoverDot).collect(Collectors.toSet()); + } + + @Override + public void setAttribute(String attributeName, Object attributeValue) { + + if (attributeValue == null) { + removeAttribute(coverDot(attributeName)); + } else { + this.attrs.put(coverDot(attributeName), attributeValue); + } + } + + @Override + public void removeAttribute(String attributeName) { + this.attrs.remove(coverDot(attributeName)); + } + + @Override + public Instant getCreationTime() { + return Instant.ofEpochMilli(this.createdMillis); + } + + void setCreationTime(long created) { + this.createdMillis = created; + } + + @Override + public Instant getLastAccessedTime() { + return Instant.ofEpochMilli(this.accessedMillis); + } + + @Override + public void setLastAccessedTime(Instant lastAccessedTime) { + this.accessedMillis = lastAccessedTime.toEpochMilli(); + this.expireAt = Date.from(lastAccessedTime.plus(Duration.ofSeconds(this.intervalSeconds))); + } + + @Override + public Duration getMaxInactiveInterval() { + return Duration.ofSeconds(this.intervalSeconds); + } + + @Override + public void setMaxInactiveInterval(Duration interval) { + this.intervalSeconds = interval.toSeconds(); + } + + @Override + public boolean isExpired() { + return this.intervalSeconds >= 0 && Instant.now().isAfter(expireAt.toInstant()); + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MongoSession that = (MongoSession) o; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } + + @Override + public String getId() { + return this.id; + } + + Date getExpireAt() { + return this.expireAt; + } + + void setExpireAt(final Date expireAt) { + this.expireAt = expireAt; + } + + boolean hasChangedSessionId() { + return !getId().equals(this.originalSessionId); + } + + String getOriginalSessionId() { + return this.originalSessionId; + } + + /** + * Sets the session id. + * + * @param id the id to set + * @since 3.2 + */ + void setId(String id) { + this.id = id; + } + + /** + * Sets the {@link SessionIdGenerator} to use. + * + * @param sessionIdGenerator the {@link SessionIdGenerator} to use + * @since 3.2 + */ + void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { + Assert.notNull(sessionIdGenerator, "sessionIdGenerator cannot be null"); + this.sessionIdGenerator = sessionIdGenerator; + } +} diff --git a/src/main/java/org/mongodb/spring/session/MongoSessionUtils.java b/src/main/java/org/mongodb/spring/session/MongoSessionUtils.java new file mode 100644 index 0000000..415dc4b --- /dev/null +++ b/src/main/java/org/mongodb/spring/session/MongoSessionUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import com.mongodb.ConnectionString; +import com.mongodb.DBObject; +import org.bson.Document; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +/** + * Utility for MongoSession. + * + * @author Greg Turnquist + */ +final class MongoSessionUtils { + + private static final String DEFAULT_URI = "mongodb://localhost:27017"; + private static final String URI_SYSTEM_PROPERTY_NAME = "org.mongodb.test.uri"; + public static final String DEFAULT_DATABASE_NAME = "MongoSpringSessionTest"; + + private MongoSessionUtils() {} + + @Nullable static DBObject convertToDBObject(AbstractMongoSessionConverter mongoSessionConverter, MongoSession session) { + + return (DBObject) mongoSessionConverter.convert( + session, TypeDescriptor.valueOf(MongoSession.class), TypeDescriptor.valueOf(DBObject.class)); + } + + @Nullable static MongoSession convertToSession(AbstractMongoSessionConverter mongoSessionConverter, Document session) { + + return (MongoSession) mongoSessionConverter.convert( + session, TypeDescriptor.valueOf(Document.class), TypeDescriptor.valueOf(MongoSession.class)); + } + + static ConnectionString getConnectionString() { + String connectionString = System.getProperty(URI_SYSTEM_PROPERTY_NAME, DEFAULT_URI); + return new ConnectionString(connectionString); + } +} diff --git a/src/main/java/org/mongodb/spring/session/ReactiveMongoSessionRepository.java b/src/main/java/org/mongodb/spring/session/ReactiveMongoSessionRepository.java new file mode 100644 index 0000000..e52cad8 --- /dev/null +++ b/src/main/java/org/mongodb/spring/session/ReactiveMongoSessionRepository.java @@ -0,0 +1,232 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import java.time.Duration; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bson.Document; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.index.IndexOperations; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.session.MapSession; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.SessionIdGenerator; +import org.springframework.session.UuidSessionIdGenerator; +import org.springframework.session.events.SessionCreatedEvent; +import org.springframework.session.events.SessionDeletedEvent; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +/** + * A {@link ReactiveSessionRepository} implementation that uses Spring Data MongoDB. + * + * @author Greg Turnquist + * @author Vedran Pavic + * @since 2.2.0 + */ +public class ReactiveMongoSessionRepository + implements ReactiveSessionRepository, ApplicationEventPublisherAware, InitializingBean { + + /** + * The default time period in seconds in which a session will expire. + * + * @deprecated since 3.0.0 in favor of {@link MapSession#DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS} + */ + @Deprecated + public static final int DEFAULT_INACTIVE_INTERVAL = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; + + /** The default collection name for storing session. */ + public static final String DEFAULT_COLLECTION_NAME = "sessions"; + + private static final Log logger = LogFactory.getLog(ReactiveMongoSessionRepository.class); + + private final ReactiveMongoOperations mongoOperations; + + private Duration defaultMaxInactiveInterval = Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); + + private String collectionName = DEFAULT_COLLECTION_NAME; + + private AbstractMongoSessionConverter mongoSessionConverter = + new JdkMongoSessionConverter(this.defaultMaxInactiveInterval); + + @Nullable private MongoOperations blockingMongoOperations; + + @Nullable private ApplicationEventPublisher eventPublisher; + + private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance(); + + public ReactiveMongoSessionRepository(ReactiveMongoOperations mongoOperations) { + this.mongoOperations = mongoOperations; + } + + /** + * Creates a new {@link MongoSession} that is capable of being persisted by this {@link ReactiveSessionRepository}. + * + *

This allows optimizations and customizations in how the {@link MongoSession} is persisted. For example, the + * implementation returned might keep track of the changes ensuring that only the delta needs to be persisted on a + * save. + * + * @return a new {@link MongoSession} that is capable of being persisted by this {@link ReactiveSessionRepository} + */ + @Override + public Mono createSession() { + // @formatter:off + return Mono.fromSupplier(() -> this.sessionIdGenerator.generate()) + .zipWith(Mono.just(this.defaultMaxInactiveInterval.toSeconds())) + .map((tuple) -> new MongoSession(tuple.getT1(), tuple.getT2())) + .doOnNext((mongoSession) -> mongoSession.setMaxInactiveInterval(this.defaultMaxInactiveInterval)) + .doOnNext((mongoSession) -> mongoSession.setSessionIdGenerator(this.sessionIdGenerator)) + .doOnNext((mongoSession) -> publishEvent(new SessionCreatedEvent(this, mongoSession))) + .switchIfEmpty(Mono.just(new MongoSession(this.sessionIdGenerator))) + .subscribeOn(Schedulers.boundedElastic()) + .publishOn(Schedulers.parallel()); + // @formatter:on + } + + @Override + public Mono save(MongoSession session) { + + return Mono // + .justOrEmpty(MongoSessionUtils.convertToDBObject(this.mongoSessionConverter, session)) // + .flatMap((dbObject) -> { + if (session.hasChangedSessionId()) { + + return this.mongoOperations + .remove( + Query.query(Criteria.where("_id").is(session.getOriginalSessionId())), + this.collectionName) // + .then(this.mongoOperations.save(dbObject, this.collectionName)); + } else { + + return this.mongoOperations.save(dbObject, this.collectionName); + } + }) // + .then(); + } + + @Override + public Mono findById(String id) { + + return findSession(id) // + .map((document) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, document)) // + .filter((mongoSession) -> !mongoSession.isExpired()) // + .doOnNext((mongoSession) -> mongoSession.setSessionIdGenerator(this.sessionIdGenerator)) + .switchIfEmpty(Mono.defer(() -> this.deleteById(id).then(Mono.empty()))); + } + + @Override + public Mono deleteById(String id) { + + return findSession(id) // + .flatMap((document) -> this.mongoOperations + .remove(document, this.collectionName) // + .then(Mono.just(document))) // + .map((document) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, document)) // + .doOnNext((mongoSession) -> publishEvent(new SessionDeletedEvent(this, mongoSession))) // + .then(); + } + + /** + * Do not use {@link org.springframework.data.mongodb.core.index.ReactiveIndexOperations} to ensure indexes exist. + * Instead, get a blocking {@link IndexOperations} and use that instead, if possible. + */ + @Override + public void afterPropertiesSet() { + + if (this.blockingMongoOperations != null) { + + IndexOperations indexOperations = this.blockingMongoOperations.indexOps(this.collectionName); + this.mongoSessionConverter.ensureIndexes(indexOperations); + } + } + + private Mono findSession(String id) { + return this.mongoOperations.findById(id, Document.class, this.collectionName); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + private void publishEvent(ApplicationEvent event) { + if (this.eventPublisher == null) { + logger.error("Error publishing " + event + ". No event publisher set."); + } else { + try { + this.eventPublisher.publishEvent(event); + } catch (Throwable ex) { + logger.error("Error publishing " + event + ".", ex); + } + } + } + + /** + * Set the maximum inactive interval in seconds between requests before newly created sessions will be invalidated. + * A negative time indicates that the session will never time out. The default is 30 minutes. + * + * @param defaultMaxInactiveInterval the default maxInactiveInterval + */ + public void setDefaultMaxInactiveInterval(Duration defaultMaxInactiveInterval) { + Assert.notNull(defaultMaxInactiveInterval, "defaultMaxInactiveInterval must not be null"); + this.defaultMaxInactiveInterval = defaultMaxInactiveInterval; + } + + /** + * Set the maximum inactive interval in seconds between requests before newly created sessions will be invalidated. + * A negative time indicates that the session will never time out. The default is 1800 (30 minutes). + * + * @param defaultMaxInactiveInterval the default maxInactiveInterval in seconds + * @deprecated since 3.0.0, in favor of {@link #setDefaultMaxInactiveInterval(Duration)} + */ + @Deprecated(since = "3.0.0") + @SuppressWarnings("InlineMeSuggester") + public void setMaxInactiveIntervalInSeconds(Integer defaultMaxInactiveInterval) { + setDefaultMaxInactiveInterval(Duration.ofSeconds(defaultMaxInactiveInterval)); + } + + public String getCollectionName() { + return this.collectionName; + } + + public void setCollectionName(final String collectionName) { + this.collectionName = collectionName; + } + + public void setMongoSessionConverter(final AbstractMongoSessionConverter mongoSessionConverter) { + this.mongoSessionConverter = mongoSessionConverter; + } + + public void setBlockingMongoOperations(final MongoOperations blockingMongoOperations) { + this.blockingMongoOperations = blockingMongoOperations; + } + + public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { + Assert.notNull(sessionIdGenerator, "sessionIdGenerator cannot be null"); + this.sessionIdGenerator = sessionIdGenerator; + } +} diff --git a/src/main/java/org/springframework/session/data/mongo/config/annotation/web/http/EnableMongoHttpSession.java b/src/main/java/org/mongodb/spring/session/config/annotation/web/http/EnableMongoHttpSession.java similarity index 64% rename from src/main/java/org/springframework/session/data/mongo/config/annotation/web/http/EnableMongoHttpSession.java rename to src/main/java/org/mongodb/spring/session/config/annotation/web/http/EnableMongoHttpSession.java index a314592..35f1ed7 100644 --- a/src/main/java/org/springframework/session/data/mongo/config/annotation/web/http/EnableMongoHttpSession.java +++ b/src/main/java/org/mongodb/spring/session/config/annotation/web/http/EnableMongoHttpSession.java @@ -1,11 +1,12 @@ /* + * Copyright 2025-present MongoDB, Inc. * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,23 +15,21 @@ * limitations under the License. */ -package org.springframework.session.data.mongo.config.annotation.web.http; +package org.mongodb.spring.session.config.annotation.web.http; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - +import org.mongodb.spring.session.MongoIndexedSessionRepository; import org.springframework.context.annotation.Import; import org.springframework.session.MapSession; -import org.springframework.session.data.mongo.MongoIndexedSessionRepository; /** - * Add this annotation to a {@code @Configuration} class to expose the - * SessionRepositoryFilter as a bean named "springSessionRepositoryFilter" and backed by - * Mongo. Use {@code collectionName} to change default name of the collection used to - * store sessions. + * Add this annotation to a {@code @Configuration} class to expose the SessionRepositoryFilter as a bean named + * "springSessionRepositoryFilter" and backed by Mongo. Use {@code collectionName} to change default name of the + * collection used to store sessions. * *

  * 
@@ -55,16 +54,17 @@
 @Import(MongoHttpSessionConfiguration.class)
 public @interface EnableMongoHttpSession {
 
-	/**
-	 * The maximum time a session will be kept if it is inactive.
-	 * @return default max inactive interval in seconds
-	 */
-	int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
-
-	/**
-	 * The collection name to use.
-	 * @return name of the collection to store session
-	 */
-	String collectionName() default MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME;
+    /**
+     * The maximum time a session will be kept if it is inactive.
+     *
+     * @return default max inactive interval in seconds
+     */
+    int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
 
+    /**
+     * The collection name to use.
+     *
+     * @return name of the collection to store session
+     */
+    String collectionName() default MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME;
 }
diff --git a/src/main/java/org/mongodb/spring/session/config/annotation/web/http/MongoHttpSessionConfiguration.java b/src/main/java/org/mongodb/spring/session/config/annotation/web/http/MongoHttpSessionConfiguration.java
new file mode 100644
index 0000000..041f472
--- /dev/null
+++ b/src/main/java/org/mongodb/spring/session/config/annotation/web/http/MongoHttpSessionConfiguration.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ * Copyright 2014-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.mongodb.spring.session.config.annotation.web.http;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.mongodb.spring.session.AbstractMongoSessionConverter;
+import org.mongodb.spring.session.JdkMongoSessionConverter;
+import org.mongodb.spring.session.MongoIndexedSessionRepository;
+import org.springframework.beans.factory.BeanClassLoaderAware;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.EmbeddedValueResolverAware;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.ImportAware;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.serializer.support.DeserializingConverter;
+import org.springframework.core.serializer.support.SerializingConverter;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.lang.Nullable;
+import org.springframework.session.IndexResolver;
+import org.springframework.session.MapSession;
+import org.springframework.session.Session;
+import org.springframework.session.SessionIdGenerator;
+import org.springframework.session.UuidSessionIdGenerator;
+import org.springframework.session.config.SessionRepositoryCustomizer;
+import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.util.StringValueResolver;
+
+/**
+ * Configuration class registering {@code MongoSessionRepository} bean. To import this configuration use
+ * {@link EnableMongoHttpSession} annotation.
+ *
+ * @author Jakub Kubrynski
+ * @author Eddú Meléndez
+ * @since 1.2
+ */
+@Configuration(proxyBeanMethods = false)
+@Import(SpringHttpSessionConfiguration.class)
+public class MongoHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
+
+    private AbstractMongoSessionConverter mongoSessionConverter;
+
+    private Duration maxInactiveInterval = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL;
+
+    @Nullable private String collectionName;
+
+    @Nullable private StringValueResolver embeddedValueResolver;
+
+    @Nullable private List> sessionRepositoryCustomizers;
+
+    @Nullable private ClassLoader classLoader;
+
+    @Nullable private IndexResolver indexResolver;
+
+    private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance();
+
+    @Bean
+    @SuppressWarnings("NullAway")
+    public MongoIndexedSessionRepository mongoSessionRepository(MongoOperations mongoOperations) {
+
+        MongoIndexedSessionRepository repository = new MongoIndexedSessionRepository(mongoOperations);
+        repository.setDefaultMaxInactiveInterval(this.maxInactiveInterval);
+
+        if (this.mongoSessionConverter != null) {
+            repository.setMongoSessionConverter(this.mongoSessionConverter);
+
+            if (this.indexResolver != null) {
+                this.mongoSessionConverter.setIndexResolver(this.indexResolver);
+            }
+        } else {
+            JdkMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(
+                    new SerializingConverter(),
+                    new DeserializingConverter(this.classLoader),
+                    Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS));
+
+            if (this.indexResolver != null) {
+                mongoSessionConverter.setIndexResolver(this.indexResolver);
+            }
+
+            repository.setMongoSessionConverter(mongoSessionConverter);
+        }
+
+        if (StringUtils.hasText(this.collectionName)) {
+            repository.setCollectionName(this.collectionName);
+        }
+        repository.setSessionIdGenerator(this.sessionIdGenerator);
+
+        Assert.notNull(this.sessionRepositoryCustomizers, "SessionRepositoryCustomizers not initialized.");
+        this.sessionRepositoryCustomizers.forEach(
+                (sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(repository));
+
+        return repository;
+    }
+
+    public void setCollectionName(String collectionName) {
+        this.collectionName = collectionName;
+    }
+
+    public void setMaxInactiveInterval(Duration maxInactiveInterval) {
+        this.maxInactiveInterval = maxInactiveInterval;
+    }
+
+    @Deprecated
+    @SuppressWarnings("InlineMeSuggester")
+    public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
+        setMaxInactiveInterval(Duration.ofSeconds(maxInactiveIntervalInSeconds));
+    }
+
+    @Override
+    @SuppressWarnings("NullAway")
+    public void setImportMetadata(AnnotationMetadata importMetadata) {
+
+        AnnotationAttributes attributes = AnnotationAttributes.fromMap(
+                importMetadata.getAnnotationAttributes(EnableMongoHttpSession.class.getName()));
+
+        if (attributes != null) {
+            this.maxInactiveInterval =
+                    Duration.ofSeconds(attributes.getNumber("maxInactiveIntervalInSeconds"));
+        }
+
+        String collectionNameValue = (attributes != null) ? attributes.getString("collectionName") : "";
+        if (StringUtils.hasText(collectionNameValue)) {
+            Assert.notNull(this.embeddedValueResolver, "EmbeddedValueResolver not initialized.");
+            this.collectionName = this.embeddedValueResolver.resolveStringValue(collectionNameValue);
+        }
+    }
+
+    @Autowired(required = false)
+    public void setMongoSessionConverter(AbstractMongoSessionConverter mongoSessionConverter) {
+        this.mongoSessionConverter = mongoSessionConverter;
+    }
+
+    @Autowired(required = false)
+    public void setSessionRepositoryCustomizers(
+            ObjectProvider> sessionRepositoryCustomizers) {
+        this.sessionRepositoryCustomizers =
+                sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
+    }
+
+    @Override
+    public void setBeanClassLoader(ClassLoader classLoader) {
+        this.classLoader = classLoader;
+    }
+
+    @Override
+    public void setEmbeddedValueResolver(StringValueResolver resolver) {
+        this.embeddedValueResolver = resolver;
+    }
+
+    @Autowired(required = false)
+    public void setIndexResolver(IndexResolver indexResolver) {
+        this.indexResolver = indexResolver;
+    }
+
+    @Autowired(required = false)
+    public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) {
+        this.sessionIdGenerator = sessionIdGenerator;
+    }
+}
diff --git a/src/main/java/org/springframework/session/data/mongo/config/annotation/web/reactive/EnableMongoWebSession.java b/src/main/java/org/mongodb/spring/session/config/annotation/web/reactive/EnableMongoWebSession.java
similarity index 59%
rename from src/main/java/org/springframework/session/data/mongo/config/annotation/web/reactive/EnableMongoWebSession.java
rename to src/main/java/org/mongodb/spring/session/config/annotation/web/reactive/EnableMongoWebSession.java
index 9581e45..07b543f 100644
--- a/src/main/java/org/springframework/session/data/mongo/config/annotation/web/reactive/EnableMongoWebSession.java
+++ b/src/main/java/org/mongodb/spring/session/config/annotation/web/reactive/EnableMongoWebSession.java
@@ -1,11 +1,12 @@
 /*
+ * Copyright 2025-present MongoDB, Inc.
  * Copyright 2014-present the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *      https://www.apache.org/licenses/LICENSE-2.0
+ *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,21 +15,19 @@
  * limitations under the License.
  */
 
-package org.springframework.session.data.mongo.config.annotation.web.reactive;
+package org.mongodb.spring.session.config.annotation.web.reactive;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
-
+import org.mongodb.spring.session.ReactiveMongoSessionRepository;
 import org.springframework.context.annotation.Import;
 import org.springframework.session.MapSession;
-import org.springframework.session.data.mongo.ReactiveMongoSessionRepository;
 
 /**
- * Add this annotation to a {@code @Configuration} class to configure a MongoDB-based
- * {@code WebSessionManager} for a WebFlux application. This annotation assumes a
- * {@code ReactorMongoOperations} is defined somewhere in the application context. If not,
- * it will fail with a clear error messages. For example:
+ * Add this annotation to a {@code @Configuration} class to configure a MongoDB-based {@code WebSessionManager} for a
+ * WebFlux application. This annotation assumes a {@code ReactorMongoOperations} is defined somewhere in the application
+ * context. If not, it will fail with a clear error messages. For example:
  *
  * 
  * 
@@ -49,21 +48,22 @@
  * @since 2.0
  */
 @Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
-@Target({ java.lang.annotation.ElementType.TYPE })
+@Target({java.lang.annotation.ElementType.TYPE})
 @Documented
 @Import(ReactiveMongoWebSessionConfiguration.class)
 public @interface EnableMongoWebSession {
 
-	/**
-	 * The maximum time a session will be kept if it is inactive.
-	 * @return default max inactive interval in seconds
-	 */
-	int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
-
-	/**
-	 * The collection name to use.
-	 * @return name of the collection to store session
-	 */
-	String collectionName() default ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME;
+    /**
+     * The maximum time a session will be kept if it is inactive.
+     *
+     * @return default max inactive interval in seconds
+     */
+    int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
 
+    /**
+     * The collection name to use.
+     *
+     * @return name of the collection to store session
+     */
+    String collectionName() default ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME;
 }
diff --git a/src/main/java/org/mongodb/spring/session/config/annotation/web/reactive/ReactiveMongoWebSessionConfiguration.java b/src/main/java/org/mongodb/spring/session/config/annotation/web/reactive/ReactiveMongoWebSessionConfiguration.java
new file mode 100644
index 0000000..43f4972
--- /dev/null
+++ b/src/main/java/org/mongodb/spring/session/config/annotation/web/reactive/ReactiveMongoWebSessionConfiguration.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ * Copyright 2014-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.mongodb.spring.session.config.annotation.web.reactive;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.mongodb.spring.session.AbstractMongoSessionConverter;
+import org.mongodb.spring.session.JdkMongoSessionConverter;
+import org.mongodb.spring.session.ReactiveMongoSessionRepository;
+import org.springframework.beans.factory.BeanClassLoaderAware;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.EmbeddedValueResolverAware;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.ImportAware;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.serializer.support.DeserializingConverter;
+import org.springframework.core.serializer.support.SerializingConverter;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.ReactiveMongoOperations;
+import org.springframework.lang.Nullable;
+import org.springframework.session.IndexResolver;
+import org.springframework.session.MapSession;
+import org.springframework.session.Session;
+import org.springframework.session.SessionIdGenerator;
+import org.springframework.session.UuidSessionIdGenerator;
+import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
+import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.util.StringValueResolver;
+
+/**
+ * Configure a {@link ReactiveMongoSessionRepository} using a provided {@link ReactiveMongoOperations}.
+ *
+ * @author Greg Turnquist
+ * @author Vedran Pavic
+ */
+@Configuration(proxyBeanMethods = false)
+@Import(SpringWebSessionConfiguration.class)
+public class ReactiveMongoWebSessionConfiguration
+        implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
+
+    @Nullable private AbstractMongoSessionConverter mongoSessionConverter;
+
+    private Duration maxInactiveInterval = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL;
+
+    @Nullable private String collectionName;
+
+    @Nullable private StringValueResolver embeddedValueResolver;
+
+    private List> sessionRepositoryCustomizers;
+
+    @Autowired(required = false)
+    @Nullable private MongoOperations mongoOperations;
+
+    @Nullable private ClassLoader classLoader;
+
+    @Nullable private IndexResolver indexResolver;
+
+    private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance();
+
+    @Bean
+    public ReactiveMongoSessionRepository reactiveMongoSessionRepository(ReactiveMongoOperations operations) {
+
+        ReactiveMongoSessionRepository repository = new ReactiveMongoSessionRepository(operations);
+
+        if (this.mongoSessionConverter != null) {
+            repository.setMongoSessionConverter(this.mongoSessionConverter);
+
+            if (this.indexResolver != null) {
+                this.mongoSessionConverter.setIndexResolver(this.indexResolver);
+            }
+
+        } else {
+            JdkMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(
+                    new SerializingConverter(),
+                    new DeserializingConverter(this.classLoader),
+                    Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS));
+
+            if (this.indexResolver != null) {
+                mongoSessionConverter.setIndexResolver(this.indexResolver);
+            }
+
+            repository.setMongoSessionConverter(mongoSessionConverter);
+        }
+
+        repository.setDefaultMaxInactiveInterval(this.maxInactiveInterval);
+
+        if (this.collectionName != null) {
+            repository.setCollectionName(this.collectionName);
+        }
+
+        if (this.mongoOperations != null) {
+            repository.setBlockingMongoOperations(this.mongoOperations);
+        }
+
+        this.sessionRepositoryCustomizers.forEach(
+                (sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(repository));
+
+        repository.setSessionIdGenerator(this.sessionIdGenerator);
+
+        return repository;
+    }
+
+    @Autowired(required = false)
+    public void setMongoSessionConverter(AbstractMongoSessionConverter mongoSessionConverter) {
+        this.mongoSessionConverter = mongoSessionConverter;
+    }
+
+    @Override
+    @SuppressWarnings("NullAway")
+    public void setImportMetadata(AnnotationMetadata importMetadata) {
+
+        AnnotationAttributes attributes = AnnotationAttributes.fromMap(
+                importMetadata.getAnnotationAttributes(EnableMongoWebSession.class.getName()));
+
+        if (attributes != null) {
+            this.maxInactiveInterval =
+                    Duration.ofSeconds(attributes.getNumber("maxInactiveIntervalInSeconds"));
+        }
+
+        String collectionNameValue = (attributes != null) ? attributes.getString("collectionName") : "";
+        if (StringUtils.hasText(collectionNameValue)) {
+            Assert.notNull(this.embeddedValueResolver, "EmbeddedValueResolver not initialized.");
+            this.collectionName = this.embeddedValueResolver.resolveStringValue(collectionNameValue);
+        }
+    }
+
+    @Override
+    public void setBeanClassLoader(ClassLoader classLoader) {
+        this.classLoader = classLoader;
+    }
+
+    @Override
+    public void setEmbeddedValueResolver(StringValueResolver embeddedValueResolver) {
+        this.embeddedValueResolver = embeddedValueResolver;
+    }
+
+    public Duration getMaxInactiveInterval() {
+        return this.maxInactiveInterval;
+    }
+
+    public void setMaxInactiveInterval(Duration maxInactiveInterval) {
+        this.maxInactiveInterval = maxInactiveInterval;
+    }
+
+    @Deprecated
+    @SuppressWarnings("InlineMeSuggester")
+    public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
+        setMaxInactiveInterval(Duration.ofSeconds(maxInactiveIntervalInSeconds));
+    }
+
+    @Nullable public String getCollectionName() {
+        return this.collectionName;
+    }
+
+    public void setCollectionName(String collectionName) {
+        this.collectionName = collectionName;
+    }
+
+    @Autowired(required = false)
+    public void setSessionRepositoryCustomizers(
+            ObjectProvider>
+                    sessionRepositoryCustomizers) {
+        this.sessionRepositoryCustomizers =
+                sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
+    }
+
+    @Autowired(required = false)
+    public void setIndexResolver(IndexResolver indexResolver) {
+        this.indexResolver = indexResolver;
+    }
+
+    @Autowired(required = false)
+    public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) {
+        this.sessionIdGenerator = sessionIdGenerator;
+    }
+}
diff --git a/src/main/java/org/springframework/session/data/mongo/package-info.java b/src/main/java/org/mongodb/spring/session/package-info.java
similarity index 94%
rename from src/main/java/org/springframework/session/data/mongo/package-info.java
rename to src/main/java/org/mongodb/spring/session/package-info.java
index f7881e7..c4c4138 100644
--- a/src/main/java/org/springframework/session/data/mongo/package-info.java
+++ b/src/main/java/org/mongodb/spring/session/package-info.java
@@ -20,6 +20,6 @@
  * @author Greg Turnquist
  */
 @NonNullApi
-package org.springframework.session.data.mongo;
+package org.mongodb.spring.session;
 
 import org.springframework.lang.NonNullApi;
diff --git a/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java b/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java
deleted file mode 100644
index ef7c328..0000000
--- a/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright 2014-present the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.session.data.mongo;
-
-import java.util.Collections;
-import java.util.Set;
-
-import com.mongodb.DBObject;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.bson.Document;
-
-import org.springframework.core.convert.TypeDescriptor;
-import org.springframework.core.convert.converter.GenericConverter;
-import org.springframework.data.domain.Sort;
-import org.springframework.data.mongodb.core.index.Index;
-import org.springframework.data.mongodb.core.index.IndexInfo;
-import org.springframework.data.mongodb.core.index.IndexOperations;
-import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
-import org.springframework.session.DelegatingIndexResolver;
-import org.springframework.session.FindByIndexNameSessionRepository;
-import org.springframework.session.IndexResolver;
-import org.springframework.session.PrincipalNameIndexResolver;
-import org.springframework.session.Session;
-import org.springframework.util.Assert;
-
-/**
- * Base class for serializing and deserializing session objects. To create custom
- * serializer you have to implement this interface and simply register your class as a
- * bean.
- *
- * @author Jakub Kubrynski
- * @author Greg Turnquist
- * @since 1.2
- */
-public abstract class AbstractMongoSessionConverter implements GenericConverter {
-
-	static final String EXPIRE_AT_FIELD_NAME = "expireAt";
-
-	private static final Log LOG = LogFactory.getLog(AbstractMongoSessionConverter.class);
-
-	private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
-
-	private IndexResolver indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
-
-	/**
-	 * Returns query to be executed to return sessions based on a particular index.
-	 * @param indexName name of the index
-	 * @param indexValue value to query against
-	 * @return built query or null if indexName is not supported
-	 */
-	@Nullable
-	protected abstract Query getQueryForIndex(String indexName, Object indexValue);
-
-	/**
-	 * Method ensures that there is a TTL index on {@literal expireAt} field. It's has
-	 * {@literal expireAfterSeconds} set to zero seconds, so the expiration time is
-	 * controlled by the application. It can be extended in custom converters when there
-	 * is a need for creating additional custom indexes.
-	 * @param sessionCollectionIndexes {@link IndexOperations} to use
-	 */
-	protected void ensureIndexes(IndexOperations sessionCollectionIndexes) {
-
-		for (IndexInfo info : sessionCollectionIndexes.getIndexInfo()) {
-			if (EXPIRE_AT_FIELD_NAME.equals(info.getName())) {
-				LOG.debug("TTL index on field " + EXPIRE_AT_FIELD_NAME + " already exists");
-				return;
-			}
-		}
-
-		LOG.info("Creating TTL index on field " + EXPIRE_AT_FIELD_NAME);
-
-		sessionCollectionIndexes
-			.ensureIndex(new Index(EXPIRE_AT_FIELD_NAME, Sort.Direction.ASC).named(EXPIRE_AT_FIELD_NAME).expire(0));
-	}
-
-	protected String extractPrincipal(MongoSession expiringSession) {
-
-		return this.indexResolver.resolveIndexesFor(expiringSession)
-			.get(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
-	}
-
-	public Set getConvertibleTypes() {
-
-		return Collections.singleton(new ConvertiblePair(DBObject.class, MongoSession.class));
-	}
-
-	@SuppressWarnings("unchecked")
-	@Nullable
-	public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
-
-		if (source == null) {
-			return null;
-		}
-
-		if (DBObject.class.isAssignableFrom(sourceType.getType())) {
-			return convert(new Document(((DBObject) source).toMap()));
-		}
-		else if (Document.class.isAssignableFrom(sourceType.getType())) {
-			return convert((Document) source);
-		}
-		else {
-			return convert((MongoSession) source);
-		}
-	}
-
-	protected abstract DBObject convert(MongoSession session);
-
-	protected abstract MongoSession convert(Document sessionWrapper);
-
-	public void setIndexResolver(IndexResolver indexResolver) {
-		Assert.notNull(indexResolver, "indexResolver must not be null");
-		this.indexResolver = indexResolver;
-	}
-
-}
diff --git a/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java b/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java
deleted file mode 100644
index f66333c..0000000
--- a/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright 2014-present the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.session.data.mongo;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.Module;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.PropertyNamingStrategies;
-import com.mongodb.BasicDBObject;
-import com.mongodb.DBObject;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.bson.Document;
-import org.bson.json.JsonMode;
-import org.bson.json.JsonWriterSettings;
-
-import org.springframework.data.mongodb.core.query.Criteria;
-import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
-import org.springframework.security.jackson2.SecurityJackson2Modules;
-import org.springframework.session.FindByIndexNameSessionRepository;
-import org.springframework.util.Assert;
-
-/**
- * {@code AbstractMongoSessionConverter} implementation using Jackson.
- *
- * @author Jakub Kubrynski
- * @author Greg Turnquist
- * @author Michael Ruf
- * @since 1.2
- */
-public class JacksonMongoSessionConverter extends AbstractMongoSessionConverter {
-
-	private static final Log LOG = LogFactory.getLog(JacksonMongoSessionConverter.class);
-
-	private static final String ATTRS_FIELD_NAME = "attrs.";
-
-	private static final String PRINCIPAL_FIELD_NAME = "principal";
-
-	private static final String EXPIRE_AT_FIELD_NAME = "expireAt";
-
-	private final ObjectMapper objectMapper;
-
-	public JacksonMongoSessionConverter() {
-		this(Collections.emptyList());
-	}
-
-	public JacksonMongoSessionConverter(Iterable modules) {
-
-		this.objectMapper = buildObjectMapper();
-		this.objectMapper.registerModules(modules);
-	}
-
-	public JacksonMongoSessionConverter(ObjectMapper objectMapper) {
-
-		Assert.notNull(objectMapper, "ObjectMapper can NOT be null!");
-		this.objectMapper = objectMapper;
-	}
-
-	@Nullable
-	protected Query getQueryForIndex(String indexName, Object indexValue) {
-
-		if (FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
-			return Query.query(Criteria.where(PRINCIPAL_FIELD_NAME).is(indexValue));
-		}
-		else {
-			return Query.query(Criteria.where(ATTRS_FIELD_NAME + MongoSession.coverDot(indexName)).is(indexValue));
-		}
-	}
-
-	private ObjectMapper buildObjectMapper() {
-
-		ObjectMapper objectMapper = new ObjectMapper();
-
-		// serialize fields instead of properties
-		objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
-		objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
-
-		// ignore unresolved fields (mostly 'principal')
-		objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-
-		objectMapper.setPropertyNamingStrategy(new MongoIdNamingStrategy());
-
-		objectMapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));
-		objectMapper.addMixIn(MongoSession.class, MongoSessionMixin.class);
-		objectMapper.addMixIn(HashMap.class, HashMapMixin.class);
-
-		return objectMapper;
-	}
-
-	@Override
-	protected DBObject convert(MongoSession source) {
-
-		try {
-			DBObject dbSession = BasicDBObject.parse(this.objectMapper.writeValueAsString(source));
-
-			// Override default serialization with proper values.
-			dbSession.put(PRINCIPAL_FIELD_NAME, extractPrincipal(source));
-			dbSession.put(EXPIRE_AT_FIELD_NAME, source.getExpireAt());
-			return dbSession;
-		}
-		catch (JsonProcessingException ex) {
-			throw new IllegalStateException("Cannot convert MongoExpiringSession", ex);
-		}
-	}
-
-	@Override
-	@Nullable
-	protected MongoSession convert(Document source) {
-
-		Date expireAt = (Date) source.remove(EXPIRE_AT_FIELD_NAME);
-		source.remove("originalSessionId");
-		String json = source.toJson(JsonWriterSettings.builder().outputMode(JsonMode.RELAXED).build());
-
-		try {
-			MongoSession mongoSession = this.objectMapper.readValue(json, MongoSession.class);
-			mongoSession.setExpireAt(expireAt);
-			return mongoSession;
-		}
-		catch (IOException ex) {
-			LOG.error("Error during Mongo Session deserialization", ex);
-			return null;
-		}
-	}
-
-	/**
-	 * Used to whitelist {@link MongoSession} for {@link SecurityJackson2Modules}.
-	 */
-	private static class MongoSessionMixin {
-
-		@JsonCreator
-		MongoSessionMixin(@JsonProperty("_id") String id,
-				@JsonProperty("intervalSeconds") long maxInactiveIntervalInSeconds) {
-		}
-
-	}
-
-	/**
-	 * Used to whitelist {@link HashMap} for {@link SecurityJackson2Modules}.
-	 */
-	private static class HashMapMixin {
-
-		// Nothing special
-
-	}
-
-	private static class MongoIdNamingStrategy extends PropertyNamingStrategies.NamingBase {
-
-		@Override
-		public String translate(String propertyName) {
-
-			switch (propertyName) {
-				case "id":
-					return "_id";
-				case "_id":
-					return "id";
-				default:
-					return propertyName;
-			}
-		}
-
-	}
-
-}
diff --git a/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java b/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java
deleted file mode 100644
index 19a8b56..0000000
--- a/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright 2014-present the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.session.data.mongo;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-
-import com.mongodb.BasicDBObject;
-import com.mongodb.DBObject;
-import org.bson.Document;
-import org.bson.types.Binary;
-
-import org.springframework.core.convert.converter.Converter;
-import org.springframework.core.serializer.support.DeserializingConverter;
-import org.springframework.core.serializer.support.SerializingConverter;
-import org.springframework.data.mongodb.core.query.Criteria;
-import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.lang.Nullable;
-import org.springframework.session.FindByIndexNameSessionRepository;
-import org.springframework.session.Session;
-import org.springframework.util.Assert;
-
-/**
- * {@code AbstractMongoSessionConverter} implementation using standard Java serialization.
- *
- * @author Jakub Kubrynski
- * @author Rob Winch
- * @author Greg Turnquist
- * @since 1.2
- */
-public class JdkMongoSessionConverter extends AbstractMongoSessionConverter {
-
-	private static final String ID = "_id";
-
-	private static final String CREATION_TIME = "created";
-
-	private static final String LAST_ACCESSED_TIME = "accessed";
-
-	private static final String MAX_INTERVAL = "interval";
-
-	private static final String ATTRIBUTES = "attr";
-
-	private static final String PRINCIPAL_FIELD_NAME = "principal";
-
-	private final Converter serializer;
-
-	private final Converter deserializer;
-
-	private Duration maxInactiveInterval;
-
-	public JdkMongoSessionConverter(Duration maxInactiveInterval) {
-		this(new SerializingConverter(), new DeserializingConverter(), maxInactiveInterval);
-	}
-
-	public JdkMongoSessionConverter(Converter serializer, Converter deserializer,
-			Duration maxInactiveInterval) {
-
-		Assert.notNull(serializer, "serializer cannot be null");
-		Assert.notNull(deserializer, "deserializer cannot be null");
-		Assert.notNull(maxInactiveInterval, "maxInactiveInterval cannot be null");
-
-		this.serializer = serializer;
-		this.deserializer = deserializer;
-		this.maxInactiveInterval = maxInactiveInterval;
-	}
-
-	@Override
-	@Nullable
-	public Query getQueryForIndex(String indexName, Object indexValue) {
-
-		if (FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
-			return Query.query(Criteria.where(PRINCIPAL_FIELD_NAME).is(indexValue));
-		}
-		else {
-			return null;
-		}
-	}
-
-	@Override
-	protected DBObject convert(MongoSession session) {
-
-		BasicDBObject basicDBObject = new BasicDBObject();
-
-		basicDBObject.put(ID, session.getId());
-		basicDBObject.put(CREATION_TIME, session.getCreationTime());
-		basicDBObject.put(LAST_ACCESSED_TIME, session.getLastAccessedTime());
-		basicDBObject.put(MAX_INTERVAL, session.getMaxInactiveInterval());
-		basicDBObject.put(PRINCIPAL_FIELD_NAME, extractPrincipal(session));
-		basicDBObject.put(EXPIRE_AT_FIELD_NAME, session.getExpireAt());
-		basicDBObject.put(ATTRIBUTES, serializeAttributes(session));
-
-		return basicDBObject;
-	}
-
-	@Override
-	protected MongoSession convert(Document sessionWrapper) {
-
-		Object maxInterval = sessionWrapper.getOrDefault(MAX_INTERVAL, this.maxInactiveInterval);
-
-		Duration maxIntervalDuration = (maxInterval instanceof Duration) ? (Duration) maxInterval
-				: Duration.parse(maxInterval.toString());
-
-		MongoSession session = new MongoSession(sessionWrapper.getString(ID), maxIntervalDuration.getSeconds());
-
-		Object creationTime = sessionWrapper.get(CREATION_TIME);
-		if (creationTime instanceof Instant) {
-			session.setCreationTime(((Instant) creationTime).toEpochMilli());
-		}
-		else if (creationTime instanceof Date) {
-			session.setCreationTime(((Date) creationTime).getTime());
-		}
-
-		Object lastAccessedTime = sessionWrapper.get(LAST_ACCESSED_TIME);
-		if (lastAccessedTime instanceof Instant) {
-			session.setLastAccessedTime((Instant) lastAccessedTime);
-		}
-		else if (lastAccessedTime instanceof Date) {
-			session.setLastAccessedTime(Instant.ofEpochMilli(((Date) lastAccessedTime).getTime()));
-		}
-
-		session.setExpireAt((Date) sessionWrapper.get(EXPIRE_AT_FIELD_NAME));
-
-		deserializeAttributes(sessionWrapper, session);
-
-		return session;
-	}
-
-	@Nullable
-	private byte[] serializeAttributes(Session session) {
-
-		Map attributes = new HashMap<>();
-
-		for (String attrName : session.getAttributeNames()) {
-			attributes.put(attrName, session.getAttribute(attrName));
-		}
-
-		return this.serializer.convert(attributes);
-	}
-
-	@SuppressWarnings("unchecked")
-	private void deserializeAttributes(Document sessionWrapper, Session session) {
-
-		Object sessionAttributes = sessionWrapper.get(ATTRIBUTES);
-
-		byte[] attributesBytes = ((sessionAttributes instanceof Binary) ? ((Binary) sessionAttributes).getData()
-				: (byte[]) sessionAttributes);
-
-		Map attributes = (Map) this.deserializer.convert(attributesBytes);
-
-		if (attributes != null) {
-			for (Map.Entry entry : attributes.entrySet()) {
-				session.setAttribute(entry.getKey(), entry.getValue());
-			}
-		}
-	}
-
-}
diff --git a/src/main/java/org/springframework/session/data/mongo/MongoIndexedSessionRepository.java b/src/main/java/org/springframework/session/data/mongo/MongoIndexedSessionRepository.java
deleted file mode 100644
index 2cfea19..0000000
--- a/src/main/java/org/springframework/session/data/mongo/MongoIndexedSessionRepository.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright 2014-present the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.session.data.mongo;
-
-import java.time.Duration;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import com.mongodb.DBObject;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.bson.Document;
-
-import org.springframework.beans.factory.InitializingBean;
-import org.springframework.context.ApplicationEvent;
-import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.context.ApplicationEventPublisherAware;
-import org.springframework.data.mongodb.core.MongoOperations;
-import org.springframework.data.mongodb.core.index.IndexOperations;
-import org.springframework.lang.Nullable;
-import org.springframework.session.FindByIndexNameSessionRepository;
-import org.springframework.session.MapSession;
-import org.springframework.session.SessionIdGenerator;
-import org.springframework.session.UuidSessionIdGenerator;
-import org.springframework.session.events.SessionCreatedEvent;
-import org.springframework.session.events.SessionDeletedEvent;
-import org.springframework.session.events.SessionExpiredEvent;
-import org.springframework.util.Assert;
-
-/**
- * Session repository implementation which stores sessions in Mongo. Uses
- * {@link AbstractMongoSessionConverter} to transform session objects from/to native Mongo
- * representation ({@code DBObject}). Repository is also responsible for removing expired
- * sessions from database. Cleanup is done every minute.
- *
- * @author Jakub Kubrynski
- * @author Greg Turnquist
- * @author Vedran Pavic
- * @since 2.2.0
- */
-public class MongoIndexedSessionRepository
-		implements FindByIndexNameSessionRepository, ApplicationEventPublisherAware, InitializingBean {
-
-	/**
-	 * The default time period in seconds in which a session will expire.
-	 * @deprecated since 3.0.0 in favor of
-	 * {@link MapSession#DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS}
-	 */
-	@Deprecated
-	public static final int DEFAULT_INACTIVE_INTERVAL = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
-
-	/**
-	 * the default collection name for storing session.
-	 */
-	public static final String DEFAULT_COLLECTION_NAME = "sessions";
-
-	private static final Log logger = LogFactory.getLog(MongoIndexedSessionRepository.class);
-
-	private final MongoOperations mongoOperations;
-
-	private Duration defaultMaxInactiveInterval = Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
-
-	private String collectionName = DEFAULT_COLLECTION_NAME;
-
-	private AbstractMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(
-			this.defaultMaxInactiveInterval);
-
-	private ApplicationEventPublisher eventPublisher;
-
-	private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance();
-
-	public MongoIndexedSessionRepository(MongoOperations mongoOperations) {
-		this.mongoOperations = mongoOperations;
-	}
-
-	@Override
-	public MongoSession createSession() {
-
-		MongoSession session = new MongoSession(this.sessionIdGenerator, this.defaultMaxInactiveInterval.toSeconds());
-
-		publishEvent(new SessionCreatedEvent(this, session));
-
-		return session;
-	}
-
-	@Override
-	public void save(MongoSession session) {
-		DBObject dbObject = MongoSessionUtils.convertToDBObject(this.mongoSessionConverter, session);
-		Assert.notNull(dbObject, "dbObject must not be null");
-		this.mongoOperations.save(dbObject, this.collectionName);
-	}
-
-	@Override
-	@Nullable
-	public MongoSession findById(String id) {
-
-		Document sessionWrapper = findSession(id);
-
-		if (sessionWrapper == null) {
-			return null;
-		}
-
-		MongoSession session = MongoSessionUtils.convertToSession(this.mongoSessionConverter, sessionWrapper);
-
-		if (session != null) {
-			if (session.isExpired()) {
-				publishEvent(new SessionExpiredEvent(this, session));
-				deleteById(id);
-				return null;
-			}
-			session.setSessionIdGenerator(this.sessionIdGenerator);
-		}
-
-		return session;
-	}
-
-	/**
-	 * Currently this repository allows only querying against
-	 * {@code PRINCIPAL_NAME_INDEX_NAME}.
-	 * @param indexName the name if the index (i.e.
-	 * {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME})
-	 * @param indexValue the value of the index to search for.
-	 * @return sessions map
-	 */
-	@Override
-	public Map findByIndexNameAndIndexValue(String indexName, String indexValue) {
-
-		return Optional.ofNullable(this.mongoSessionConverter.getQueryForIndex(indexName, indexValue))
-			.map((query) -> this.mongoOperations.find(query, Document.class, this.collectionName))
-			.orElse(Collections.emptyList())
-			.stream()
-			.map((dbSession) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, dbSession))
-			.peek((session) -> session.setSessionIdGenerator(this.sessionIdGenerator))
-			.collect(Collectors.toMap(MongoSession::getId, (mapSession) -> mapSession));
-	}
-
-	@Override
-	public void deleteById(String id) {
-
-		Optional.ofNullable(findSession(id)).ifPresent((document) -> {
-
-			MongoSession session = MongoSessionUtils.convertToSession(this.mongoSessionConverter, document);
-			if (session != null) {
-				publishEvent(new SessionDeletedEvent(this, session));
-			}
-			this.mongoOperations.remove(document, this.collectionName);
-		});
-	}
-
-	@Override
-	public void afterPropertiesSet() {
-
-		IndexOperations indexOperations = this.mongoOperations.indexOps(this.collectionName);
-		this.mongoSessionConverter.ensureIndexes(indexOperations);
-	}
-
-	@Nullable
-	private Document findSession(String id) {
-		return this.mongoOperations.findById(id, Document.class, this.collectionName);
-	}
-
-	@Override
-	public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
-		this.eventPublisher = eventPublisher;
-	}
-
-	private void publishEvent(ApplicationEvent event) {
-
-		try {
-			this.eventPublisher.publishEvent(event);
-		}
-		catch (Throwable ex) {
-			logger.error("Error publishing " + event + ".", ex);
-		}
-	}
-
-	/**
-	 * Set the maximum inactive interval in seconds between requests before newly created
-	 * sessions will be invalidated. A negative time indicates that the session will never
-	 * time out. The default is 30 minutes.
-	 * @param defaultMaxInactiveInterval the default maxInactiveInterval
-	 */
-	public void setDefaultMaxInactiveInterval(Duration defaultMaxInactiveInterval) {
-		org.springframework.util.Assert.notNull(defaultMaxInactiveInterval,
-				"defaultMaxInactiveInterval must not be null");
-		this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
-	}
-
-	/**
-	 * Set the maximum inactive interval in seconds between requests before newly created
-	 * sessions will be invalidated. A negative time indicates that the session will never
-	 * time out. The default is 1800 (30 minutes).
-	 * @param defaultMaxInactiveInterval the default maxInactiveInterval in seconds
-	 * @deprecated since 3.0.0, in favor of
-	 * {@link #setDefaultMaxInactiveInterval(Duration)}
-	 */
-	@Deprecated(since = "3.0.0")
-	public void setMaxInactiveIntervalInSeconds(Integer defaultMaxInactiveInterval) {
-		setDefaultMaxInactiveInterval(Duration.ofSeconds(defaultMaxInactiveInterval));
-	}
-
-	public void setCollectionName(final String collectionName) {
-		this.collectionName = collectionName;
-	}
-
-	public void setMongoSessionConverter(final AbstractMongoSessionConverter mongoSessionConverter) {
-		this.mongoSessionConverter = mongoSessionConverter;
-	}
-
-	/**
-	 * Set the {@link SessionIdGenerator} to use to generate session ids.
-	 * @param sessionIdGenerator the {@link SessionIdGenerator} to use
-	 * @since 3.2
-	 */
-	public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) {
-		Assert.notNull(sessionIdGenerator, "sessionIdGenerator cannot be null");
-		this.sessionIdGenerator = sessionIdGenerator;
-	}
-
-}
diff --git a/src/main/java/org/springframework/session/data/mongo/MongoSession.java b/src/main/java/org/springframework/session/data/mongo/MongoSession.java
deleted file mode 100644
index 698f463..0000000
--- a/src/main/java/org/springframework/session/data/mongo/MongoSession.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * Copyright 2014-present the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.session.data.mongo;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import org.springframework.lang.Nullable;
-import org.springframework.session.MapSession;
-import org.springframework.session.Session;
-import org.springframework.session.SessionIdGenerator;
-import org.springframework.session.UuidSessionIdGenerator;
-import org.springframework.util.Assert;
-
-/**
- * Session object providing additional information about the datetime of expiration.
- *
- * @author Jakub Kubrynski
- * @author Greg Turnquist
- * @since 1.2
- */
-public final class MongoSession implements Session {
-
-	/**
-	 * Mongo doesn't support {@literal dot} in field names. We replace it with a unicode
-	 * character from the Private Use Area.
-	 * 

- * NOTE: This was originally stored in unicode format. Delomboking the code caused it - * to get converted to another encoding, which isn't supported on all systems, so we - * migrated back to unicode. The same character is being represented ensuring binary - * compatibility. See https://www.compart.com/en/unicode/U+F607 - */ - private static final char DOT_COVER_CHAR = '\uF607'; - - private String id; - - private final String originalSessionId; - - private long createdMillis = System.currentTimeMillis(); - - private long accessedMillis; - - private long intervalSeconds; - - private Date expireAt; - - private final Map attrs = new HashMap<>(); - - private transient SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance(); - - /** - * Constructs a new instance using the provided session id. - * @param sessionId the session id to use - * @since 3.2 - */ - public MongoSession(String sessionId) { - this(sessionId, MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); - } - - public MongoSession() { - this(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); - } - - public MongoSession(long maxInactiveIntervalInSeconds) { - this(UuidSessionIdGenerator.getInstance().generate(), maxInactiveIntervalInSeconds); - } - - public MongoSession(String id, long maxInactiveIntervalInSeconds) { - this.id = id; - this.originalSessionId = id; - this.intervalSeconds = maxInactiveIntervalInSeconds; - setLastAccessedTime(Instant.ofEpochMilli(this.createdMillis)); - } - - /** - * Constructs a new instance using the provided {@link SessionIdGenerator}. - * @param sessionIdGenerator the {@link SessionIdGenerator} to use - * @since 3.2 - */ - public MongoSession(SessionIdGenerator sessionIdGenerator) { - this(sessionIdGenerator.generate(), MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); - this.sessionIdGenerator = sessionIdGenerator; - } - - /** - * Constructs a new instance using the provided {@link SessionIdGenerator} and max - * inactive interval. - * @param sessionIdGenerator the {@link SessionIdGenerator} to use - * @param maxInactiveIntervalInSeconds the max inactive interval in seconds - * @since 3.2 - */ - MongoSession(SessionIdGenerator sessionIdGenerator, long maxInactiveIntervalInSeconds) { - this(sessionIdGenerator.generate(), maxInactiveIntervalInSeconds); - this.sessionIdGenerator = sessionIdGenerator; - } - - static String coverDot(String attributeName) { - return attributeName.replace('.', DOT_COVER_CHAR); - } - - static String uncoverDot(String attributeName) { - return attributeName.replace(DOT_COVER_CHAR, '.'); - } - - @Override - public String changeSessionId() { - - String changedId = this.sessionIdGenerator.generate(); - this.id = changedId; - return changedId; - } - - @Override - @Nullable - @SuppressWarnings("unchecked") - public T getAttribute(String attributeName) { - return (T) this.attrs.get(coverDot(attributeName)); - } - - @Override - public Set getAttributeNames() { - return this.attrs.keySet().stream().map(MongoSession::uncoverDot).collect(Collectors.toSet()); - } - - @Override - public void setAttribute(String attributeName, Object attributeValue) { - - if (attributeValue == null) { - removeAttribute(coverDot(attributeName)); - } - else { - this.attrs.put(coverDot(attributeName), attributeValue); - } - } - - @Override - public void removeAttribute(String attributeName) { - this.attrs.remove(coverDot(attributeName)); - } - - @Override - public Instant getCreationTime() { - return Instant.ofEpochMilli(this.createdMillis); - } - - void setCreationTime(long created) { - this.createdMillis = created; - } - - @Override - public Instant getLastAccessedTime() { - return Instant.ofEpochMilli(this.accessedMillis); - } - - @Override - public void setLastAccessedTime(Instant lastAccessedTime) { - this.accessedMillis = lastAccessedTime.toEpochMilli(); - this.expireAt = Date.from(lastAccessedTime.plus(Duration.ofSeconds(this.intervalSeconds))); - } - - @Override - public Duration getMaxInactiveInterval() { - return Duration.ofSeconds(this.intervalSeconds); - } - - @Override - public void setMaxInactiveInterval(Duration interval) { - this.intervalSeconds = interval.getSeconds(); - } - - @Override - public boolean isExpired() { - return this.intervalSeconds >= 0 && new Date().after(this.expireAt); - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - MongoSession that = (MongoSession) o; - return Objects.equals(this.id, that.id); - } - - @Override - public int hashCode() { - return Objects.hash(this.id); - } - - @Override - public String getId() { - return this.id; - } - - Date getExpireAt() { - return this.expireAt; - } - - void setExpireAt(final Date expireAt) { - this.expireAt = expireAt; - } - - boolean hasChangedSessionId() { - return !getId().equals(this.originalSessionId); - } - - String getOriginalSessionId() { - return this.originalSessionId; - } - - /** - * Sets the session id. - * @param id the id to set - * @since 3.2 - */ - void setId(String id) { - this.id = id; - } - - /** - * Sets the {@link SessionIdGenerator} to use. - * @param sessionIdGenerator the {@link SessionIdGenerator} to use - * @since 3.2 - */ - void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { - Assert.notNull(sessionIdGenerator, "sessionIdGenerator cannot be null"); - this.sessionIdGenerator = sessionIdGenerator; - } - -} diff --git a/src/main/java/org/springframework/session/data/mongo/MongoSessionUtils.java b/src/main/java/org/springframework/session/data/mongo/MongoSessionUtils.java deleted file mode 100644 index 4021b27..0000000 --- a/src/main/java/org/springframework/session/data/mongo/MongoSessionUtils.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import com.mongodb.DBObject; -import org.bson.Document; - -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; - -/** - * Utility for MongoSession. - * - * @author Greg Turnquist - */ -final class MongoSessionUtils { - - private MongoSessionUtils() { - } - - @Nullable - static DBObject convertToDBObject(AbstractMongoSessionConverter mongoSessionConverter, MongoSession session) { - - return (DBObject) mongoSessionConverter.convert(session, TypeDescriptor.valueOf(MongoSession.class), - TypeDescriptor.valueOf(DBObject.class)); - } - - @Nullable - static MongoSession convertToSession(AbstractMongoSessionConverter mongoSessionConverter, Document session) { - - return (MongoSession) mongoSessionConverter.convert(session, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class)); - } - -} diff --git a/src/main/java/org/springframework/session/data/mongo/ReactiveMongoSessionRepository.java b/src/main/java/org/springframework/session/data/mongo/ReactiveMongoSessionRepository.java deleted file mode 100644 index a988f3c..0000000 --- a/src/main/java/org/springframework/session/data/mongo/ReactiveMongoSessionRepository.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.time.Duration; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.bson.Document; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.ReactiveMongoOperations; -import org.springframework.data.mongodb.core.index.IndexOperations; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.session.MapSession; -import org.springframework.session.ReactiveSessionRepository; -import org.springframework.session.SessionIdGenerator; -import org.springframework.session.UuidSessionIdGenerator; -import org.springframework.session.events.SessionCreatedEvent; -import org.springframework.session.events.SessionDeletedEvent; -import org.springframework.util.Assert; - -/** - * A {@link ReactiveSessionRepository} implementation that uses Spring Data MongoDB. - * - * @author Greg Turnquist - * @author Vedran Pavic - * @since 2.2.0 - */ -public class ReactiveMongoSessionRepository - implements ReactiveSessionRepository, ApplicationEventPublisherAware, InitializingBean { - - /** - * The default time period in seconds in which a session will expire. - * @deprecated since 3.0.0 in favor of - * {@link MapSession#DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS} - */ - @Deprecated - public static final int DEFAULT_INACTIVE_INTERVAL = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; - - /** - * The default collection name for storing session. - */ - public static final String DEFAULT_COLLECTION_NAME = "sessions"; - - private static final Log logger = LogFactory.getLog(ReactiveMongoSessionRepository.class); - - private final ReactiveMongoOperations mongoOperations; - - private Duration defaultMaxInactiveInterval = Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); - - private String collectionName = DEFAULT_COLLECTION_NAME; - - private AbstractMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter( - this.defaultMaxInactiveInterval); - - private MongoOperations blockingMongoOperations; - - private ApplicationEventPublisher eventPublisher; - - private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance(); - - public ReactiveMongoSessionRepository(ReactiveMongoOperations mongoOperations) { - this.mongoOperations = mongoOperations; - } - - /** - * Creates a new {@link MongoSession} that is capable of being persisted by this - * {@link ReactiveSessionRepository}. - *

- * This allows optimizations and customizations in how the {@link MongoSession} is - * persisted. For example, the implementation returned might keep track of the changes - * ensuring that only the delta needs to be persisted on a save. - *

- * @return a new {@link MongoSession} that is capable of being persisted by this - * {@link ReactiveSessionRepository} - */ - @Override - public Mono createSession() { - // @formatter:off - return Mono.fromSupplier(() -> this.sessionIdGenerator.generate()) - .zipWith(Mono.just(this.defaultMaxInactiveInterval.toSeconds())) - .map((tuple) -> new MongoSession(tuple.getT1(), tuple.getT2())) - .doOnNext((mongoSession) -> mongoSession.setMaxInactiveInterval(this.defaultMaxInactiveInterval)) - .doOnNext( - (mongoSession) -> mongoSession.setSessionIdGenerator(this.sessionIdGenerator)) - .doOnNext((mongoSession) -> publishEvent(new SessionCreatedEvent(this, mongoSession))) - .switchIfEmpty(Mono.just(new MongoSession(this.sessionIdGenerator))) - .subscribeOn(Schedulers.boundedElastic()) - .publishOn(Schedulers.parallel()); - // @formatter:on - } - - @Override - public Mono save(MongoSession session) { - - return Mono // - .justOrEmpty(MongoSessionUtils.convertToDBObject(this.mongoSessionConverter, session)) // - .flatMap((dbObject) -> { - if (session.hasChangedSessionId()) { - - return this.mongoOperations - .remove(Query.query(Criteria.where("_id").is(session.getOriginalSessionId())), - this.collectionName) // - .then(this.mongoOperations.save(dbObject, this.collectionName)); - } - else { - - return this.mongoOperations.save(dbObject, this.collectionName); - } - }) // - .then(); - } - - @Override - public Mono findById(String id) { - - return findSession(id) // - .map((document) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, document)) // - .filter((mongoSession) -> !mongoSession.isExpired()) // - .doOnNext((mongoSession) -> mongoSession.setSessionIdGenerator(this.sessionIdGenerator)) - .switchIfEmpty(Mono.defer(() -> this.deleteById(id).then(Mono.empty()))); - } - - @Override - public Mono deleteById(String id) { - - return findSession(id) // - .flatMap((document) -> this.mongoOperations.remove(document, this.collectionName) // - .then(Mono.just(document))) // - .map((document) -> MongoSessionUtils.convertToSession(this.mongoSessionConverter, document)) // - .doOnNext((mongoSession) -> publishEvent(new SessionDeletedEvent(this, mongoSession))) // - .then(); - } - - /** - * Do not use - * {@link org.springframework.data.mongodb.core.index.ReactiveIndexOperations} to - * ensure indexes exist. Instead, get a blocking {@link IndexOperations} and use that - * instead, if possible. - */ - @Override - public void afterPropertiesSet() { - - if (this.blockingMongoOperations != null) { - - IndexOperations indexOperations = this.blockingMongoOperations.indexOps(this.collectionName); - this.mongoSessionConverter.ensureIndexes(indexOperations); - } - } - - private Mono findSession(String id) { - return this.mongoOperations.findById(id, Document.class, this.collectionName); - } - - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { - this.eventPublisher = eventPublisher; - } - - private void publishEvent(ApplicationEvent event) { - - try { - this.eventPublisher.publishEvent(event); - } - catch (Throwable ex) { - logger.error("Error publishing " + event + ".", ex); - } - } - - /** - * Set the maximum inactive interval in seconds between requests before newly created - * sessions will be invalidated. A negative time indicates that the session will never - * time out. The default is 30 minutes. - * @param defaultMaxInactiveInterval the default maxInactiveInterval - */ - public void setDefaultMaxInactiveInterval(Duration defaultMaxInactiveInterval) { - Assert.notNull(defaultMaxInactiveInterval, "defaultMaxInactiveInterval must not be null"); - this.defaultMaxInactiveInterval = defaultMaxInactiveInterval; - } - - /** - * Set the maximum inactive interval in seconds between requests before newly created - * sessions will be invalidated. A negative time indicates that the session will never - * time out. The default is 1800 (30 minutes). - * @param defaultMaxInactiveInterval the default maxInactiveInterval in seconds - * @deprecated since 3.0.0, in favor of - * {@link #setDefaultMaxInactiveInterval(Duration)} - */ - @Deprecated(since = "3.0.0") - public void setMaxInactiveIntervalInSeconds(Integer defaultMaxInactiveInterval) { - setDefaultMaxInactiveInterval(Duration.ofSeconds(defaultMaxInactiveInterval)); - } - - public String getCollectionName() { - return this.collectionName; - } - - public void setCollectionName(final String collectionName) { - this.collectionName = collectionName; - } - - public void setMongoSessionConverter(final AbstractMongoSessionConverter mongoSessionConverter) { - this.mongoSessionConverter = mongoSessionConverter; - } - - public void setBlockingMongoOperations(final MongoOperations blockingMongoOperations) { - this.blockingMongoOperations = blockingMongoOperations; - } - - public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { - Assert.notNull(sessionIdGenerator, "sessionIdGenerator cannot be null"); - this.sessionIdGenerator = sessionIdGenerator; - } - -} diff --git a/src/main/java/org/springframework/session/data/mongo/config/annotation/web/http/MongoHttpSessionConfiguration.java b/src/main/java/org/springframework/session/data/mongo/config/annotation/web/http/MongoHttpSessionConfiguration.java deleted file mode 100644 index a2c7e35..0000000 --- a/src/main/java/org/springframework/session/data/mongo/config/annotation/web/http/MongoHttpSessionConfiguration.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo.config.annotation.web.http; - -import java.time.Duration; -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.EmbeddedValueResolverAware; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportAware; -import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.serializer.support.DeserializingConverter; -import org.springframework.core.serializer.support.SerializingConverter; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.session.IndexResolver; -import org.springframework.session.MapSession; -import org.springframework.session.Session; -import org.springframework.session.SessionIdGenerator; -import org.springframework.session.UuidSessionIdGenerator; -import org.springframework.session.config.SessionRepositoryCustomizer; -import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; -import org.springframework.session.data.mongo.AbstractMongoSessionConverter; -import org.springframework.session.data.mongo.JdkMongoSessionConverter; -import org.springframework.session.data.mongo.MongoIndexedSessionRepository; -import org.springframework.util.StringUtils; -import org.springframework.util.StringValueResolver; - -/** - * Configuration class registering {@code MongoSessionRepository} bean. To import this - * configuration use {@link EnableMongoHttpSession} annotation. - * - * @author Jakub Kubrynski - * @author Eddú Meléndez - * @since 1.2 - */ -@Configuration(proxyBeanMethods = false) -@Import(SpringHttpSessionConfiguration.class) -public class MongoHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware { - - private AbstractMongoSessionConverter mongoSessionConverter; - - private Duration maxInactiveInterval = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL; - - private String collectionName; - - private StringValueResolver embeddedValueResolver; - - private List> sessionRepositoryCustomizers; - - private ClassLoader classLoader; - - private IndexResolver indexResolver; - - private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance(); - - @Bean - public MongoIndexedSessionRepository mongoSessionRepository(MongoOperations mongoOperations) { - - MongoIndexedSessionRepository repository = new MongoIndexedSessionRepository(mongoOperations); - repository.setDefaultMaxInactiveInterval(this.maxInactiveInterval); - - if (this.mongoSessionConverter != null) { - repository.setMongoSessionConverter(this.mongoSessionConverter); - - if (this.indexResolver != null) { - this.mongoSessionConverter.setIndexResolver(this.indexResolver); - } - } - else { - JdkMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(new SerializingConverter(), - new DeserializingConverter(this.classLoader), - Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS)); - - if (this.indexResolver != null) { - mongoSessionConverter.setIndexResolver(this.indexResolver); - } - - repository.setMongoSessionConverter(mongoSessionConverter); - } - - if (StringUtils.hasText(this.collectionName)) { - repository.setCollectionName(this.collectionName); - } - repository.setSessionIdGenerator(this.sessionIdGenerator); - - this.sessionRepositoryCustomizers - .forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(repository)); - - return repository; - } - - public void setCollectionName(String collectionName) { - this.collectionName = collectionName; - } - - public void setMaxInactiveInterval(Duration maxInactiveInterval) { - this.maxInactiveInterval = maxInactiveInterval; - } - - @Deprecated - public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) { - setMaxInactiveInterval(Duration.ofSeconds(maxInactiveIntervalInSeconds)); - } - - public void setImportMetadata(AnnotationMetadata importMetadata) { - - AnnotationAttributes attributes = AnnotationAttributes - .fromMap(importMetadata.getAnnotationAttributes(EnableMongoHttpSession.class.getName())); - - if (attributes != null) { - this.maxInactiveInterval = Duration - .ofSeconds(attributes.getNumber("maxInactiveIntervalInSeconds")); - } - - String collectionNameValue = (attributes != null) ? attributes.getString("collectionName") : ""; - if (StringUtils.hasText(collectionNameValue)) { - this.collectionName = this.embeddedValueResolver.resolveStringValue(collectionNameValue); - } - } - - @Autowired(required = false) - public void setMongoSessionConverter(AbstractMongoSessionConverter mongoSessionConverter) { - this.mongoSessionConverter = mongoSessionConverter; - } - - @Autowired(required = false) - public void setSessionRepositoryCustomizers( - ObjectProvider> sessionRepositoryCustomizers) { - this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList()); - } - - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - this.classLoader = classLoader; - } - - @Override - public void setEmbeddedValueResolver(StringValueResolver resolver) { - this.embeddedValueResolver = resolver; - } - - @Autowired(required = false) - public void setIndexResolver(IndexResolver indexResolver) { - this.indexResolver = indexResolver; - } - - @Autowired(required = false) - public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { - this.sessionIdGenerator = sessionIdGenerator; - } - -} diff --git a/src/main/java/org/springframework/session/data/mongo/config/annotation/web/reactive/ReactiveMongoWebSessionConfiguration.java b/src/main/java/org/springframework/session/data/mongo/config/annotation/web/reactive/ReactiveMongoWebSessionConfiguration.java deleted file mode 100644 index 08d70bf..0000000 --- a/src/main/java/org/springframework/session/data/mongo/config/annotation/web/reactive/ReactiveMongoWebSessionConfiguration.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo.config.annotation.web.reactive; - -import java.time.Duration; -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.EmbeddedValueResolverAware; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportAware; -import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.serializer.support.DeserializingConverter; -import org.springframework.core.serializer.support.SerializingConverter; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.ReactiveMongoOperations; -import org.springframework.session.IndexResolver; -import org.springframework.session.MapSession; -import org.springframework.session.Session; -import org.springframework.session.SessionIdGenerator; -import org.springframework.session.UuidSessionIdGenerator; -import org.springframework.session.config.ReactiveSessionRepositoryCustomizer; -import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration; -import org.springframework.session.data.mongo.AbstractMongoSessionConverter; -import org.springframework.session.data.mongo.JdkMongoSessionConverter; -import org.springframework.session.data.mongo.ReactiveMongoSessionRepository; -import org.springframework.util.StringUtils; -import org.springframework.util.StringValueResolver; - -/** - * Configure a {@link ReactiveMongoSessionRepository} using a provided - * {@link ReactiveMongoOperations}. - * - * @author Greg Turnquist - * @author Vedran Pavic - */ -@Configuration(proxyBeanMethods = false) -@Import(SpringWebSessionConfiguration.class) -public class ReactiveMongoWebSessionConfiguration - implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware { - - private AbstractMongoSessionConverter mongoSessionConverter; - - private Duration maxInactiveInterval = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL; - - private String collectionName; - - private StringValueResolver embeddedValueResolver; - - private List> sessionRepositoryCustomizers; - - @Autowired(required = false) - private MongoOperations mongoOperations; - - private ClassLoader classLoader; - - private IndexResolver indexResolver; - - private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance(); - - @Bean - public ReactiveMongoSessionRepository reactiveMongoSessionRepository(ReactiveMongoOperations operations) { - - ReactiveMongoSessionRepository repository = new ReactiveMongoSessionRepository(operations); - - if (this.mongoSessionConverter != null) { - repository.setMongoSessionConverter(this.mongoSessionConverter); - - if (this.indexResolver != null) { - this.mongoSessionConverter.setIndexResolver(this.indexResolver); - } - - } - else { - JdkMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(new SerializingConverter(), - new DeserializingConverter(this.classLoader), - Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS)); - - if (this.indexResolver != null) { - mongoSessionConverter.setIndexResolver(this.indexResolver); - } - - repository.setMongoSessionConverter(mongoSessionConverter); - } - - repository.setDefaultMaxInactiveInterval(this.maxInactiveInterval); - - if (this.collectionName != null) { - repository.setCollectionName(this.collectionName); - } - - if (this.mongoOperations != null) { - repository.setBlockingMongoOperations(this.mongoOperations); - } - - this.sessionRepositoryCustomizers - .forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(repository)); - - repository.setSessionIdGenerator(this.sessionIdGenerator); - - return repository; - } - - @Autowired(required = false) - public void setMongoSessionConverter(AbstractMongoSessionConverter mongoSessionConverter) { - this.mongoSessionConverter = mongoSessionConverter; - } - - @Override - public void setImportMetadata(AnnotationMetadata importMetadata) { - - AnnotationAttributes attributes = AnnotationAttributes - .fromMap(importMetadata.getAnnotationAttributes(EnableMongoWebSession.class.getName())); - - if (attributes != null) { - this.maxInactiveInterval = Duration - .ofSeconds(attributes.getNumber("maxInactiveIntervalInSeconds")); - } - - String collectionNameValue = (attributes != null) ? attributes.getString("collectionName") : ""; - if (StringUtils.hasText(collectionNameValue)) { - this.collectionName = this.embeddedValueResolver.resolveStringValue(collectionNameValue); - } - - } - - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - this.classLoader = classLoader; - } - - @Override - public void setEmbeddedValueResolver(StringValueResolver embeddedValueResolver) { - this.embeddedValueResolver = embeddedValueResolver; - } - - public Duration getMaxInactiveInterval() { - return this.maxInactiveInterval; - } - - public void setMaxInactiveInterval(Duration maxInactiveInterval) { - this.maxInactiveInterval = maxInactiveInterval; - } - - @Deprecated - public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) { - setMaxInactiveInterval(Duration.ofSeconds(maxInactiveIntervalInSeconds)); - } - - public String getCollectionName() { - return this.collectionName; - } - - public void setCollectionName(String collectionName) { - this.collectionName = collectionName; - } - - @Autowired(required = false) - public void setSessionRepositoryCustomizers( - ObjectProvider> sessionRepositoryCustomizers) { - this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList()); - } - - @Autowired(required = false) - public void setIndexResolver(IndexResolver indexResolver) { - this.indexResolver = indexResolver; - } - - @Autowired(required = false) - public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { - this.sessionIdGenerator = sessionIdGenerator; - } - -} diff --git a/src/test/java/org/mongodb/spring/session/AbstractMongoSessionConverterTests.java b/src/test/java/org/mongodb/spring/session/AbstractMongoSessionConverterTests.java new file mode 100644 index 0000000..767682f --- /dev/null +++ b/src/test/java/org/mongodb/spring/session/AbstractMongoSessionConverterTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.mongodb.DBObject; +import java.time.Duration; +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.session.FindByIndexNameSessionRepository; + +/** @author Greg Turnquist */ +public abstract class AbstractMongoSessionConverterTests { + + abstract AbstractMongoSessionConverter getMongoSessionConverter(); + + @Test + void verifyRoundTripSerialization() throws Exception { + + // given + MongoSession toSerialize = new MongoSession(); + toSerialize.setAttribute("username", "john_the_springer"); + + // when + DBObject dbObject = convertToDBObject(toSerialize); + MongoSession deserialized = convertToSession(dbObject); + + // then + assertThat(deserialized).usingRecursiveComparison().isEqualTo(toSerialize); + } + + @Test + void verifyRoundTripSecuritySerialization() { + + // given + MongoSession toSerialize = new MongoSession(); + String principalName = "john_the_springer"; + SecurityContextImpl context = new SecurityContextImpl(); + context.setAuthentication(new UsernamePasswordAuthenticationToken(principalName, null)); + toSerialize.setAttribute("SPRING_SECURITY_CONTEXT", context); + + // when + DBObject serialized = convertToDBObject(toSerialize); + MongoSession deserialized = convertToSession(serialized); + + // then + assertThat(deserialized).usingRecursiveComparison().isEqualTo(toSerialize); + + SecurityContextImpl springSecurityContextBefore = toSerialize.getAttribute("SPRING_SECURITY_CONTEXT"); + SecurityContextImpl springSecurityContextAfter = deserialized.getAttribute("SPRING_SECURITY_CONTEXT"); + + assertThat(springSecurityContextBefore).usingRecursiveComparison().isEqualTo(springSecurityContextAfter); + assertThat(springSecurityContextAfter.getAuthentication().getPrincipal()) + .isEqualTo("john_the_springer"); + assertThat(springSecurityContextAfter.getAuthentication().getCredentials()) + .isNull(); + } + + @Test + void shouldExtractPrincipalNameFromAttributes() throws Exception { + + // given + MongoSession toSerialize = new MongoSession(); + String principalName = "john_the_springer"; + toSerialize.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName); + + // when + DBObject dbObject = convertToDBObject(toSerialize); + + // then + assertThat(dbObject.get("principal")).isEqualTo(principalName); + } + + @Test + void shouldExtractPrincipalNameFromAuthentication() throws Exception { + + // given + MongoSession toSerialize = new MongoSession(); + String principalName = "john_the_springer"; + SecurityContextImpl context = new SecurityContextImpl(); + context.setAuthentication(new UsernamePasswordAuthenticationToken(principalName, null)); + toSerialize.setAttribute("SPRING_SECURITY_CONTEXT", context); + + // when + DBObject dbObject = convertToDBObject(toSerialize); + + // then + assertThat(dbObject.get("principal")).isEqualTo(principalName); + } + + @Test + void sessionWrapperWithNoMaxIntervalShouldFallbackToDefaultValues() { + + // given + MongoSession toSerialize = new MongoSession(); + DBObject dbObject = convertToDBObject(toSerialize); + Document document = new Document(dbObject.toMap()); + document.remove("interval"); + + // when + MongoSession convertedSession = getMongoSessionConverter().convert(document); + + // then + assertThat(convertedSession.getMaxInactiveInterval()).isEqualTo(Duration.ofMinutes(30)); + } + + @Nullable MongoSession convertToSession(DBObject session) { + return (MongoSession) getMongoSessionConverter() + .convert(session, TypeDescriptor.valueOf(DBObject.class), TypeDescriptor.valueOf(MongoSession.class)); + } + + @Nullable DBObject convertToDBObject(MongoSession session) { + return (DBObject) getMongoSessionConverter() + .convert(session, TypeDescriptor.valueOf(MongoSession.class), TypeDescriptor.valueOf(DBObject.class)); + } +} diff --git a/src/test/java/org/mongodb/spring/session/JacksonMongoSessionConverterTests.java b/src/test/java/org/mongodb/spring/session/JacksonMongoSessionConverterTests.java new file mode 100644 index 0000000..107ba4a --- /dev/null +++ b/src/test/java/org/mongodb/spring/session/JacksonMongoSessionConverterTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.DBObject; +import java.lang.reflect.Field; +import java.util.Date; +import java.util.HashMap; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.AssertionsForClassTypes; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.util.ReflectionUtils; + +/** + * @author Jakub Kubrynski + * @author Greg Turnquist + */ +class JacksonMongoSessionConverterTests extends AbstractMongoSessionConverterTests { + + JacksonMongoSessionConverter mongoSessionConverter = new JacksonMongoSessionConverter(); + + @Override + AbstractMongoSessionConverter getMongoSessionConverter() { + return this.mongoSessionConverter; + } + + @Test + void shouldSaveIdField() { + + // given + MongoSession session = new MongoSession(); + + // when + DBObject convert = this.mongoSessionConverter.convert(session); + + // then + AssertionsForClassTypes.assertThat(convert.get("_id")).isEqualTo(session.getId()); + AssertionsForClassTypes.assertThat(convert.get("id")).isNull(); + } + + @Test + void shouldQueryAgainstAttribute() throws Exception { + + // when + Query cart = this.mongoSessionConverter.getQueryForIndex("cart", "my-cart"); + + // then + AssertionsForClassTypes.assertThat(cart.getQueryObject().get("attrs.cart")) + .isEqualTo("my-cart"); + } + + @Test + void shouldAllowCustomObjectMapper() { + + // given + ObjectMapper myMapper = new ObjectMapper(); + + // when + JacksonMongoSessionConverter converter = new JacksonMongoSessionConverter(myMapper); + + // then + Field objectMapperField = ReflectionUtils.findField(JacksonMongoSessionConverter.class, "objectMapper"); + ReflectionUtils.makeAccessible(objectMapperField); + ObjectMapper converterMapper = (ObjectMapper) ReflectionUtils.getField(objectMapperField, converter); + + AssertionsForClassTypes.assertThat(converterMapper).isEqualTo(myMapper); + } + + @Test + void shouldNotAllowNullObjectMapperToBeInjected() { + + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> new JacksonMongoSessionConverter((ObjectMapper) null)); + } + + @Test + void shouldSaveExpireAtAsDate() { + + // given + MongoSession session = new MongoSession(); + + // when + DBObject convert = this.mongoSessionConverter.convert(session); + + // then + AssertionsForClassTypes.assertThat(convert.get("expireAt")).isInstanceOf(Date.class); + AssertionsForClassTypes.assertThat(convert.get("expireAt")).isEqualTo(session.getExpireAt()); + } + + @Test + void shouldLoadExpireAtFromDocument() { + + // given + Date now = new Date(); + HashMap data = new HashMap<>(); + + data.put("expireAt", now); + data.put("@class", MongoSession.class.getName()); + data.put("_id", new ObjectId().toString()); + + Document document = new Document(data); + + // when + MongoSession convertedSession = this.mongoSessionConverter.convert(document); + + // then + AssertionsForClassTypes.assertThat(convertedSession).isNotNull(); + AssertionsForClassTypes.assertThat(convertedSession.getExpireAt()).isEqualTo(now); + } +} diff --git a/src/test/java/org/springframework/session/data/mongo/JdkMongoSessionConverterTests.java b/src/test/java/org/mongodb/spring/session/JdkMongoSessionConverterTests.java similarity index 53% rename from src/test/java/org/springframework/session/data/mongo/JdkMongoSessionConverterTests.java rename to src/test/java/org/mongodb/spring/session/JdkMongoSessionConverterTests.java index af4b5de..6415cf0 100644 --- a/src/test/java/org/springframework/session/data/mongo/JdkMongoSessionConverterTests.java +++ b/src/test/java/org/mongodb/spring/session/JdkMongoSessionConverterTests.java @@ -1,11 +1,12 @@ /* + * Copyright 2025-present MongoDB, Inc. * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,17 +15,15 @@ * limitations under the License. */ -package org.springframework.session.data.mongo; +package org.mongodb.spring.session; -import java.time.Duration; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import java.time.Duration; import org.junit.jupiter.api.Test; - import org.springframework.core.serializer.support.DeserializingConverter; import org.springframework.core.serializer.support.SerializingConverter; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - /** * @author Jakub Kubrynski * @author Rob Winch @@ -32,25 +31,26 @@ */ class JdkMongoSessionConverterTests extends AbstractMongoSessionConverterTests { - Duration inactiveInterval = Duration.ofMinutes(30); - - JdkMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(this.inactiveInterval); + Duration inactiveInterval = Duration.ofMinutes(30); - @Override - AbstractMongoSessionConverter getMongoSessionConverter() { - return this.mongoSessionConverter; - } + JdkMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(this.inactiveInterval); - @Test - void constructorNullSerializer() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new JdkMongoSessionConverter(null, new DeserializingConverter(), this.inactiveInterval)); - } + @Override + AbstractMongoSessionConverter getMongoSessionConverter() { + return this.mongoSessionConverter; + } - @Test - void constructorNullDeserializer() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new JdkMongoSessionConverter(new SerializingConverter(), null, this.inactiveInterval)); - } + @Test + void constructorNullSerializer() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> new JdkMongoSessionConverter(null, new DeserializingConverter(), this.inactiveInterval)); + } + @Test + void constructorNullDeserializer() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> new JdkMongoSessionConverter(new SerializingConverter(), null, this.inactiveInterval)); + } } diff --git a/src/test/java/org/mongodb/spring/session/MongoIndexedSessionRepositoryTests.java b/src/test/java/org/mongodb/spring/session/MongoIndexedSessionRepositoryTests.java new file mode 100644 index 0000000..084da6e --- /dev/null +++ b/src/test/java/org/mongodb/spring/session/MongoIndexedSessionRepositoryTests.java @@ -0,0 +1,290 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.verify; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.MapSession; +import org.springframework.session.SessionIdGenerator; + +/** + * Tests for {@link MongoIndexedSessionRepository}. + * + * @author Jakub Kubrynski + * @author Vedran Pavic + * @author Greg Turnquist + */ +@ExtendWith(MockitoExtension.class) +class MongoIndexedSessionRepositoryTests { + + @Mock + private AbstractMongoSessionConverter converter; + + @Mock + private MongoOperations mongoOperations; + + private MongoIndexedSessionRepository repository; + + @BeforeEach + void setUp() { + + this.repository = new MongoIndexedSessionRepository(this.mongoOperations); + this.repository.setMongoSessionConverter(this.converter); + } + + @Test + void shouldCreateSession() { + + // when + MongoSession session = this.repository.createSession(); + + // then + assertThat(session.getId()).isNotEmpty(); + assertThat(session.getMaxInactiveInterval().getSeconds()) + .isEqualTo(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); + } + + @Test + void shouldSaveSession() { + + // given + MongoSession session = new MongoSession(); + BasicDBObject dbSession = new BasicDBObject(); + + given(this.converter.convert( + session, TypeDescriptor.valueOf(MongoSession.class), TypeDescriptor.valueOf(DBObject.class))) + .willReturn(dbSession); + // when + this.repository.save(session); + + // then + verify(this.mongoOperations).save(dbSession, MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME); + } + + @Test + void shouldGetSession() { + + // given + String sessionId = UUID.randomUUID().toString(); + Document sessionDocument = new Document(); + + given(this.mongoOperations.findById( + sessionId, Document.class, MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)) + .willReturn(sessionDocument); + + MongoSession session = new MongoSession(); + + given(this.converter.convert( + sessionDocument, + TypeDescriptor.valueOf(Document.class), + TypeDescriptor.valueOf(MongoSession.class))) + .willReturn(session); + + // when + MongoSession retrievedSession = this.repository.findById(sessionId); + + // then + assertThat(retrievedSession).isEqualTo(session); + } + + @Test + void shouldHandleExpiredSession() { + + // given + String sessionId = UUID.randomUUID().toString(); + Document sessionDocument = new Document(); + + given(this.mongoOperations.findById( + sessionId, Document.class, MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)) + .willReturn(sessionDocument); + + MongoSession session = mock(MongoSession.class); + + given(session.isExpired()).willReturn(true); + given(this.converter.convert( + sessionDocument, + TypeDescriptor.valueOf(Document.class), + TypeDescriptor.valueOf(MongoSession.class))) + .willReturn(session); + given(session.getId()).willReturn("sessionId"); + + // when + this.repository.findById(sessionId); + + // then + verify(this.mongoOperations) + .remove(any(Document.class), eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)); + } + + @Test + void shouldDeleteSession() { + + // given + String sessionId = UUID.randomUUID().toString(); + + Document sessionDocument = new Document(); + sessionDocument.put("id", sessionId); + + MongoSession mongoSession = new MongoSession(sessionId, MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); + + given(this.converter.convert( + sessionDocument, + TypeDescriptor.valueOf(Document.class), + TypeDescriptor.valueOf(MongoSession.class))) + .willReturn(mongoSession); + given(this.mongoOperations.findById( + eq(sessionId), eq(Document.class), eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME))) + .willReturn(sessionDocument); + + // when + this.repository.deleteById(sessionId); + + // then + verify(this.mongoOperations) + .remove(any(Document.class), eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)); + } + + @Test + void shouldGetSessionsMapByPrincipal() { + + // given + String principalNameIndexName = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; + + Document document = new Document(); + + given(this.converter.getQueryForIndex(anyString(), any(Object.class))).willReturn(mock(Query.class)); + given(this.mongoOperations.find( + any(Query.class), + eq(Document.class), + eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME))) + .willReturn(Collections.singletonList(document)); + + String sessionId = UUID.randomUUID().toString(); + + MongoSession session = new MongoSession(sessionId, 1800); + + given(this.converter.convert( + document, TypeDescriptor.valueOf(Document.class), TypeDescriptor.valueOf(MongoSession.class))) + .willReturn(session); + + // when + Map sessionsMap = + this.repository.findByIndexNameAndIndexValue(principalNameIndexName, "john"); + + // then + assertThat(sessionsMap).containsOnlyKeys(sessionId); + assertThat(sessionsMap).containsValues(session); + } + + @Test + void shouldReturnEmptyMapForNotSupportedIndex() { + + // given + String index = "some_not_supported_index_name"; + + // when + Map sessionsMap = this.repository.findByIndexNameAndIndexValue(index, "some_value"); + + // then + assertThat(sessionsMap).isEmpty(); + } + + @Test + void createSessionWhenSessionIdGeneratorThenUses() { + this.repository.setSessionIdGenerator(new FixedSessionIdGenerator("123")); + MongoSession session = this.repository.createSession(); + assertThat(session.getId()).isEqualTo("123"); + assertThat(session.changeSessionId()).isEqualTo("123"); + } + + @Test + void setSessionIdGeneratorWhenNullThenThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSessionIdGenerator(null)); + } + + @Test + void findByIdWhenChangeSessionIdThenUsesSessionIdGenerator() { + this.repository.setSessionIdGenerator(new FixedSessionIdGenerator("456")); + + Document sessionDocument = new Document(); + + given(this.mongoOperations.findById( + "123", Document.class, MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)) + .willReturn(sessionDocument); + + MongoSession session = new MongoSession("123"); + + given(this.converter.convert( + sessionDocument, + TypeDescriptor.valueOf(Document.class), + TypeDescriptor.valueOf(MongoSession.class))) + .willReturn(session); + + MongoSession retrievedSession = this.repository.findById("123"); + assertThat(retrievedSession.getId()).isEqualTo("123"); + String newSessionId = retrievedSession.changeSessionId(); + assertThat(newSessionId).isEqualTo("456"); + } + + @Test + void createSessionWhenMaxInactiveIntervalSetThenUse() { + this.repository.setDefaultMaxInactiveInterval(Duration.ofSeconds(60)); + MongoSession session = this.repository.createSession(); + Instant now = Instant.now(); + assertThat(session.getExpireAt()) + .isBetween(now.plusSeconds(59), Instant.now().plusSeconds(61)); + } + + static class FixedSessionIdGenerator implements SessionIdGenerator { + + private final String id; + + FixedSessionIdGenerator(String id) { + this.id = id; + } + + @Override + public String generate() { + return this.id; + } + } +} diff --git a/src/test/java/org/springframework/session/data/mongo/MongoSessionTests.java b/src/test/java/org/mongodb/spring/session/MongoSessionTests.java similarity index 65% rename from src/test/java/org/springframework/session/data/mongo/MongoSessionTests.java rename to src/test/java/org/mongodb/spring/session/MongoSessionTests.java index 6992fce..6ca08bd 100644 --- a/src/test/java/org/springframework/session/data/mongo/MongoSessionTests.java +++ b/src/test/java/org/mongodb/spring/session/MongoSessionTests.java @@ -1,11 +1,12 @@ /* + * Copyright 2025-present MongoDB, Inc. * Copyright 2014-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,29 +15,27 @@ * limitations under the License. */ -package org.springframework.session.data.mongo; +package org.mongodb.spring.session; + +import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; import java.time.Instant; - import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; - /** * @author Rob Winch * @author Greg Turnquist */ class MongoSessionTests { - @Test - void isExpiredWhenIntervalNegativeThenFalse() { - - MongoSession session = new MongoSession(); - session.setMaxInactiveInterval(Duration.ofSeconds(-1)); - session.setLastAccessedTime(Instant.ofEpochMilli(0L)); + @Test + void isExpiredWhenIntervalNegativeThenFalse() { - assertThat(session.isExpired()).isFalse(); - } + MongoSession session = new MongoSession(); + session.setMaxInactiveInterval(Duration.ofSeconds(-1)); + session.setLastAccessedTime(Instant.ofEpochMilli(0L)); + assertThat(session.isExpired()).isFalse(); + } } diff --git a/src/test/java/org/mongodb/spring/session/ReactiveMongoSessionRepositoryTests.java b/src/test/java/org/mongodb/spring/session/ReactiveMongoSessionRepositoryTests.java new file mode 100644 index 0000000..b006674 --- /dev/null +++ b/src/test/java/org/mongodb/spring/session/ReactiveMongoSessionRepositoryTests.java @@ -0,0 +1,292 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.times; +import static org.mockito.BDDMockito.verify; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.mongodb.client.result.DeleteResult; +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.index.IndexOperations; +import org.springframework.session.MapSession; +import org.springframework.session.events.SessionDeletedEvent; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * Tests for {@link ReactiveMongoSessionRepository}. + * + * @author Jakub Kubrynski + * @author Vedran Pavic + * @author Greg Turnquist + */ +@ExtendWith(MockitoExtension.class) +class ReactiveMongoSessionRepositoryTests { + + @Mock + private AbstractMongoSessionConverter converter; + + @Mock + private ReactiveMongoOperations mongoOperations; + + @Mock + private MongoOperations blockingMongoOperations; + + @Mock + private ApplicationEventPublisher eventPublisher; + + private ReactiveMongoSessionRepository repository; + + @BeforeEach + void setUp() { + + this.repository = new ReactiveMongoSessionRepository(this.mongoOperations); + this.repository.setMongoSessionConverter(this.converter); + this.repository.setApplicationEventPublisher(this.eventPublisher); + } + + @Test + void shouldCreateSession() { + + this.repository + .createSession() // + .as(StepVerifier::create) // + .expectNextMatches((mongoSession) -> { + assertThat(mongoSession.getId()).isNotEmpty(); + assertThat(mongoSession.getMaxInactiveInterval()) + .isEqualTo(Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS)); + return true; + }) // + .verifyComplete(); + } + + @Test + void shouldSaveSession() { + + // given + MongoSession session = new MongoSession(); + BasicDBObject dbSession = new BasicDBObject(); + + given(this.converter.convert( + session, TypeDescriptor.valueOf(MongoSession.class), TypeDescriptor.valueOf(DBObject.class))) + .willReturn(dbSession); + + given(this.mongoOperations.save(dbSession, "sessions")).willReturn(Mono.just(dbSession)); + + // when + this.repository + .save(session) // + .as(StepVerifier::create) // + .verifyComplete(); + + verify(this.mongoOperations).save(dbSession, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME); + } + + @Test + void shouldGetSession() { + + // given + String sessionId = UUID.randomUUID().toString(); + Document sessionDocument = new Document(); + + given(this.mongoOperations.findById( + sessionId, Document.class, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) + .willReturn(Mono.just(sessionDocument)); + + MongoSession session = new MongoSession(); + + given(this.converter.convert( + sessionDocument, + TypeDescriptor.valueOf(Document.class), + TypeDescriptor.valueOf(MongoSession.class))) + .willReturn(session); + + // when + this.repository + .findById(sessionId) // + .as(StepVerifier::create) // + .expectNext(session) // + .verifyComplete(); + } + + @Test + void shouldHandleExpiredSession() { + + // given + String sessionId = UUID.randomUUID().toString(); + Document sessionDocument = new Document(); + + given(this.mongoOperations.findById( + sessionId, Document.class, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) + .willReturn(Mono.just(sessionDocument)); + + given(this.mongoOperations.remove(sessionDocument, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) + .willReturn(Mono.just(DeleteResult.acknowledged(1))); + + MongoSession session = mock(MongoSession.class); + + given(session.isExpired()).willReturn(true); + given(this.converter.convert( + sessionDocument, + TypeDescriptor.valueOf(Document.class), + TypeDescriptor.valueOf(MongoSession.class))) + .willReturn(session); + + // when + this.repository + .findById(sessionId) // + .as(StepVerifier::create) // + .verifyComplete(); + + // then + verify(this.mongoOperations) + .remove(any(Document.class), eq(ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)); + } + + @Test + void shouldDeleteSession() { + + // given + String sessionId = UUID.randomUUID().toString(); + Document sessionDocument = new Document(); + + given(this.mongoOperations.findById( + sessionId, Document.class, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) + .willReturn(Mono.just(sessionDocument)); + + given(this.mongoOperations.remove(sessionDocument, "sessions")) + .willReturn(Mono.just(DeleteResult.acknowledged(1))); + + MongoSession session = mock(MongoSession.class); + + given(this.converter.convert( + sessionDocument, + TypeDescriptor.valueOf(Document.class), + TypeDescriptor.valueOf(MongoSession.class))) + .willReturn(session); + + // when + this.repository + .deleteById(sessionId) // + .as(StepVerifier::create) // + .verifyComplete(); + + verify(this.mongoOperations) + .remove(any(Document.class), eq(ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)); + + verify(this.eventPublisher).publishEvent(any(SessionDeletedEvent.class)); + } + + @Test + void shouldInvokeMethodToCreateIndexesImperatively() { + + // given + IndexOperations indexOperations = mock(IndexOperations.class); + given(this.blockingMongoOperations.indexOps((String) any())).willReturn(indexOperations); + + this.repository.setBlockingMongoOperations(this.blockingMongoOperations); + + // when + this.repository.afterPropertiesSet(); + + // then + verify(this.blockingMongoOperations, times(1)).indexOps((String) any()); + verify(this.converter, times(1)).ensureIndexes(indexOperations); + } + + @Test + void createSessionWhenSessionIdGeneratorThenUses() { + this.repository.setSessionIdGenerator(() -> "test"); + + this.repository + .createSession() + .as(StepVerifier::create) + .assertNext((mongoSession) -> { + assertThat(mongoSession.getId()).isEqualTo("test"); + assertThat(mongoSession.changeSessionId()).isEqualTo("test"); + }) + .verifyComplete(); + } + + @Test + void setSessionIdGeneratorWhenNullThenThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.repository.setSessionIdGenerator(null)) + .withMessage("sessionIdGenerator cannot be null"); + } + + @Test + void findByIdWhenChangeSessionIdThenUsesSessionIdGenerator() { + this.repository.setSessionIdGenerator(() -> "test"); + + String sessionId = UUID.randomUUID().toString(); + Document sessionDocument = new Document(); + + given(this.mongoOperations.findById( + sessionId, Document.class, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) + .willReturn(Mono.just(sessionDocument)); + + MongoSession session = new MongoSession(sessionId); + + given(this.converter.convert( + sessionDocument, + TypeDescriptor.valueOf(Document.class), + TypeDescriptor.valueOf(MongoSession.class))) + .willReturn(session); + + this.repository + .findById(sessionId) + .as(StepVerifier::create) + .assertNext((mongoSession) -> { + String oldId = mongoSession.getId(); + String newId = mongoSession.changeSessionId(); + assertThat(oldId).isEqualTo(sessionId); + assertThat(newId).isEqualTo("test"); + }) + .verifyComplete(); + } + + @Test + void createSessionWhenMaxInactiveIntervalSetThenUse() { + this.repository.setDefaultMaxInactiveInterval(Duration.ofSeconds(60)); + MongoSession session = this.repository.createSession().block(); + Instant now = Instant.now(); + assertThat(session.getExpireAt()) + .isBetween(now.plusSeconds(59), Instant.now().plusSeconds(61)); + } +} diff --git a/src/test/java/org/mongodb/spring/session/config/annotation/web/http/MongoHttpSessionConfigurationTests.java b/src/test/java/org/mongodb/spring/session/config/annotation/web/http/MongoHttpSessionConfigurationTests.java new file mode 100644 index 0000000..6a09df6 --- /dev/null +++ b/src/test/java/org/mongodb/spring/session/config/annotation/web/http/MongoHttpSessionConfigurationTests.java @@ -0,0 +1,373 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session.config.annotation.web.http; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; + +import java.net.UnknownHostException; +import java.time.Duration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mongodb.spring.session.AbstractMongoSessionConverter; +import org.mongodb.spring.session.JacksonMongoSessionConverter; +import org.mongodb.spring.session.MongoIndexedSessionRepository; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.index.IndexOperations; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.session.IndexResolver; +import org.springframework.session.Session; +import org.springframework.session.SessionIdGenerator; +import org.springframework.session.UuidSessionIdGenerator; +import org.springframework.session.config.SessionRepositoryCustomizer; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Tests for {@link MongoHttpSessionConfiguration}. + * + * @author Eddú Meléndez + * @author Vedran Pavic + */ +class MongoHttpSessionConfigurationTests { + + private static final String COLLECTION_NAME = "testSessions"; + + private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600; + + private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + @AfterEach + void after() { + + if (this.context != null) { + this.context.close(); + } + } + + @Test + void noMongoOperationsConfiguration() { + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> registerAndRefresh(EmptyConfiguration.class)) + .withMessageContaining("mongoSessionRepository"); + } + + @Test + void defaultConfiguration() { + + registerAndRefresh(DefaultConfiguration.class); + + assertThat(this.context.getBean(MongoIndexedSessionRepository.class)).isNotNull(); + } + + @Test + void customCollectionName() { + + registerAndRefresh(CustomCollectionNameConfiguration.class); + + MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); + + assertThat(repository).isNotNull(); + assertThat(ReflectionTestUtils.getField(repository, "collectionName")).isEqualTo(COLLECTION_NAME); + } + + @Test + void setCustomCollectionName() { + + registerAndRefresh(CustomCollectionNameSetConfiguration.class); + + MongoHttpSessionConfiguration session = this.context.getBean(MongoHttpSessionConfiguration.class); + + assertThat(session).isNotNull(); + assertThat(ReflectionTestUtils.getField(session, "collectionName")).isEqualTo(COLLECTION_NAME); + } + + @Test + void customMaxInactiveIntervalInSeconds() { + + registerAndRefresh(CustomMaxInactiveIntervalInSecondsConfiguration.class); + + MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); + + assertThat(repository) + .extracting("defaultMaxInactiveInterval") + .isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); + } + + @Test + void setCustomMaxInactiveIntervalInSeconds() { + + registerAndRefresh(CustomMaxInactiveIntervalInSecondsSetConfiguration.class); + + MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); + + assertThat(repository) + .extracting("defaultMaxInactiveInterval") + .isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); + } + + @Test + void setCustomSessionConverterConfiguration() { + + registerAndRefresh(CustomSessionConverterConfiguration.class); + + MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); + AbstractMongoSessionConverter mongoSessionConverter = this.context.getBean(AbstractMongoSessionConverter.class); + + assertThat(repository).isNotNull(); + assertThat(mongoSessionConverter).isNotNull(); + assertThat(ReflectionTestUtils.getField(repository, "mongoSessionConverter")) + .isEqualTo(mongoSessionConverter); + } + + @Test + void resolveCollectionNameByPropertyPlaceholder() { + + this.context.setEnvironment( + new MockEnvironment().withProperty("session.mongo.collectionName", COLLECTION_NAME)); + registerAndRefresh(CustomMongoJdbcSessionConfiguration.class); + + MongoHttpSessionConfiguration configuration = this.context.getBean(MongoHttpSessionConfiguration.class); + + assertThat(ReflectionTestUtils.getField(configuration, "collectionName")) + .isEqualTo(COLLECTION_NAME); + } + + @Test + void sessionRepositoryCustomizer() { + + registerAndRefresh(MongoConfiguration.class, SessionRepositoryCustomizerConfiguration.class); + + MongoIndexedSessionRepository sessionRepository = this.context.getBean(MongoIndexedSessionRepository.class); + + assertThat(sessionRepository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ofSeconds(10000)); + } + + @Test + void customIndexResolverConfigurationWithDefaultMongoSessionConverter() { + + registerAndRefresh( + MongoConfiguration.class, CustomIndexResolverConfigurationWithDefaultMongoSessionConverter.class); + + MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); + IndexResolver indexResolver = this.context.getBean(IndexResolver.class); + + assertThat(repository).isNotNull(); + assertThat(indexResolver).isNotNull(); + assertThat(repository) + .extracting("mongoSessionConverter") + .hasFieldOrPropertyWithValue("indexResolver", indexResolver); + } + + @Test + void customIndexResolverConfigurationWithProvidedMongoSessionConverter() { + + registerAndRefresh( + MongoConfiguration.class, CustomIndexResolverConfigurationWithProvidedMongoSessionConverter.class); + + MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); + IndexResolver indexResolver = this.context.getBean(IndexResolver.class); + + assertThat(repository).isNotNull(); + assertThat(indexResolver).isNotNull(); + assertThat(repository) + .extracting("mongoSessionConverter") + .hasFieldOrPropertyWithValue("indexResolver", indexResolver); + } + + @Test + void importConfigAndCustomize() { + registerAndRefresh(ImportConfigAndCustomizeConfiguration.class); + MongoIndexedSessionRepository sessionRepository = this.context.getBean(MongoIndexedSessionRepository.class); + assertThat(sessionRepository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ZERO); + } + + @Test + void registerWhenSessionIdGeneratorBeanThenUses() { + registerAndRefresh(SessionIdGeneratorConfiguration.class); + MongoIndexedSessionRepository sessionRepository = this.context.getBean(MongoIndexedSessionRepository.class); + assertThat(sessionRepository).extracting("sessionIdGenerator").isInstanceOf(TestSessionIdGenerator.class); + } + + @Test + void registerWhenNoSessionIdGeneratorBeanThenDefault() { + registerAndRefresh(DefaultConfiguration.class); + MongoIndexedSessionRepository sessionRepository = this.context.getBean(MongoIndexedSessionRepository.class); + assertThat(sessionRepository).extracting("sessionIdGenerator").isInstanceOf(UuidSessionIdGenerator.class); + } + + private void registerAndRefresh(Class... annotatedClasses) { + + this.context.register(annotatedClasses); + this.context.refresh(); + } + + @Configuration + @EnableMongoHttpSession + static class EmptyConfiguration {} + + static class BaseConfiguration { + + @Bean + MongoOperations mongoOperations() throws UnknownHostException { + + MongoOperations mongoOperations = mock(MongoOperations.class); + IndexOperations indexOperations = mock(IndexOperations.class); + + given(mongoOperations.indexOps(anyString())).willReturn(indexOperations); + + return mongoOperations; + } + } + + @Configuration + @EnableMongoHttpSession + static class DefaultConfiguration extends BaseConfiguration {} + + @Configuration + static class MongoConfiguration extends BaseConfiguration {} + + @Configuration + @EnableMongoHttpSession(collectionName = COLLECTION_NAME) + static class CustomCollectionNameConfiguration extends BaseConfiguration {} + + @Configuration + @Import(MongoConfiguration.class) + static class CustomCollectionNameSetConfiguration extends MongoHttpSessionConfiguration { + + CustomCollectionNameSetConfiguration() { + setCollectionName(COLLECTION_NAME); + } + } + + @Configuration + @EnableMongoHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS) + static class CustomMaxInactiveIntervalInSecondsConfiguration extends BaseConfiguration {} + + @Configuration + @Import(MongoConfiguration.class) + static class CustomMaxInactiveIntervalInSecondsSetConfiguration extends MongoHttpSessionConfiguration { + + CustomMaxInactiveIntervalInSecondsSetConfiguration() { + setMaxInactiveInterval(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); + } + } + + @Configuration + @Import(MongoConfiguration.class) + static class CustomSessionConverterConfiguration extends MongoHttpSessionConfiguration { + + @Bean + AbstractMongoSessionConverter mongoSessionConverter() { + return mock(AbstractMongoSessionConverter.class); + } + } + + @Configuration + @EnableMongoHttpSession(collectionName = "${session.mongo.collectionName}") + static class CustomMongoJdbcSessionConfiguration extends BaseConfiguration { + + @Bean + PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableMongoHttpSession + static class SessionRepositoryCustomizerConfiguration { + + @Bean + @Order(0) + SessionRepositoryCustomizer sessionRepositoryCustomizerOne() { + return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); + } + + @Bean + @Order(1) + SessionRepositoryCustomizer sessionRepositoryCustomizerTwo() { + return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(10000)); + } + } + + @Configuration + @EnableMongoHttpSession + static class CustomIndexResolverConfigurationWithDefaultMongoSessionConverter { + + @Bean + @SuppressWarnings("unchecked") + IndexResolver indexResolver() { + return mock(IndexResolver.class); + } + } + + @Configuration + @EnableMongoHttpSession + static class CustomIndexResolverConfigurationWithProvidedMongoSessionConverter { + + @Bean + AbstractMongoSessionConverter mongoSessionConverter() { + return new JacksonMongoSessionConverter(); + } + + @Bean + @SuppressWarnings("unchecked") + IndexResolver indexResolver() { + return mock(IndexResolver.class); + } + } + + @Configuration(proxyBeanMethods = false) + @Import(MongoHttpSessionConfiguration.class) + static class ImportConfigAndCustomizeConfiguration extends BaseConfiguration { + + @Bean + SessionRepositoryCustomizer sessionRepositoryCustomizer() { + return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableMongoHttpSession + @Import(MongoConfiguration.class) + static class SessionIdGeneratorConfiguration { + + @Bean + SessionIdGenerator sessionIdGenerator() { + return new TestSessionIdGenerator(); + } + } + + static class TestSessionIdGenerator implements SessionIdGenerator { + + @Override + public String generate() { + return "test"; + } + } +} diff --git a/src/test/java/org/mongodb/spring/session/config/annotation/web/reactive/ReactiveMongoWebSessionConfigurationTests.java b/src/test/java/org/mongodb/spring/session/config/annotation/web/reactive/ReactiveMongoWebSessionConfigurationTests.java new file mode 100644 index 0000000..96da551 --- /dev/null +++ b/src/test/java/org/mongodb/spring/session/config/annotation/web/reactive/ReactiveMongoWebSessionConfigurationTests.java @@ -0,0 +1,450 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * Copyright 2014-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.spring.session.config.annotation.web.reactive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.times; +import static org.mockito.BDDMockito.verify; + +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.Collections; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mongodb.spring.session.AbstractMongoSessionConverter; +import org.mongodb.spring.session.JacksonMongoSessionConverter; +import org.mongodb.spring.session.JdkMongoSessionConverter; +import org.mongodb.spring.session.ReactiveMongoSessionRepository; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.index.IndexOperations; +import org.springframework.session.IndexResolver; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.Session; +import org.springframework.session.SessionIdGenerator; +import org.springframework.session.UuidSessionIdGenerator; +import org.springframework.session.config.ReactiveSessionRepositoryCustomizer; +import org.springframework.session.config.annotation.web.server.EnableSpringWebSession; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.server.session.WebSessionManager; + +/** + * Verify various configurations through {@link EnableSpringWebSession}. + * + * @author Greg Turnquist + * @author Vedran Pavic + */ +class ReactiveMongoWebSessionConfigurationTests { + + private AnnotationConfigApplicationContext context; + + @AfterEach + void tearDown() { + + if (this.context != null) { + this.context.close(); + } + } + + @Test + void enableSpringWebSessionConfiguresThings() { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(GoodConfig.class); + this.context.refresh(); + + WebSessionManager webSessionManagerFoundByType = this.context.getBean(WebSessionManager.class); + Object webSessionManagerFoundByName = this.context.getBean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME); + + assertThat(webSessionManagerFoundByType).isNotNull(); + assertThat(webSessionManagerFoundByName).isNotNull(); + assertThat(webSessionManagerFoundByType).isEqualTo(webSessionManagerFoundByName); + + assertThat(this.context.getBean(ReactiveSessionRepository.class)).isNotNull(); + } + + @Test + void missingReactorSessionRepositoryBreaksAppContext() { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(BadConfig.class); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(this.context::refresh) + .withMessageContaining("Error creating bean with name 'reactiveMongoSessionRepository'") + .withMessageContaining( + "No qualifying bean of type '" + ReactiveMongoOperations.class.getCanonicalName()); + } + + @Test + void defaultSessionConverterShouldBeJdkWhenOnClasspath() throws IllegalAccessException { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(GoodConfig.class); + this.context.refresh(); + + ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); + + AbstractMongoSessionConverter converter = findMongoSessionConverter(repository); + + assertThat(converter).isOfAnyClassIn(JdkMongoSessionConverter.class); + } + + @Test + void overridingMongoSessionConverterWithBeanShouldWork() throws IllegalAccessException { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(OverrideSessionConverterConfig.class); + this.context.refresh(); + + ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); + + AbstractMongoSessionConverter converter = findMongoSessionConverter(repository); + + assertThat(converter).isOfAnyClassIn(JacksonMongoSessionConverter.class); + } + + @Test + void overridingIntervalAndCollectionNameThroughAnnotationShouldWork() { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(OverrideMongoParametersConfig.class); + this.context.refresh(); + + ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); + + assertThat(repository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ofSeconds(123)); + assertThat(repository).extracting("collectionName").isEqualTo("test-case"); + } + + @Test + void reactiveAndBlockingMongoOperationsShouldEnsureIndexing() { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(ConfigWithReactiveAndImperativeMongoOperations.class); + this.context.refresh(); + + MongoOperations operations = this.context.getBean(MongoOperations.class); + IndexOperations indexOperations = this.context.getBean(IndexOperations.class); + + // First initialization: should call createIndex once + verify(operations, times(1)).indexOps((String) any()); + verify(indexOperations, times(1)).getIndexInfo(); + verify(indexOperations, times(1)).createIndex(any()); + + // Simulate repeated initialization/configuration + this.context.close(); + this.context = new AnnotationConfigApplicationContext(); + this.context.register(ConfigWithReactiveAndImperativeMongoOperations.class); + this.context.refresh(); + + MongoOperations operations2 = this.context.getBean(MongoOperations.class); + IndexOperations indexOperations2 = this.context.getBean(IndexOperations.class); + + // Should not call createIndex again (total calls should still be 1) + verify(indexOperations2, times(1)).createIndex(any()); + } + + @Test + void overrideCollectionAndInactiveIntervalThroughConfigurationOptions() { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(CustomizedReactiveConfiguration.class); + this.context.refresh(); + + ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); + + assertThat(repository.getCollectionName()).isEqualTo("custom-collection"); + assertThat(repository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ofSeconds(123)); + } + + @Test + void sessionRepositoryCustomizer() { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(SessionRepositoryCustomizerConfiguration.class); + this.context.refresh(); + + ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); + + assertThat(repository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ofSeconds(10000)); + } + + @Test + void customIndexResolverConfigurationWithDefaultMongoSessionConverter() { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(CustomIndexResolverConfigurationWithDefaultMongoSessionConverter.class); + this.context.refresh(); + + ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); + IndexResolver indexResolver = this.context.getBean(IndexResolver.class); + + assertThat(repository).isNotNull(); + assertThat(indexResolver).isNotNull(); + assertThat(repository) + .extracting("mongoSessionConverter") + .hasFieldOrPropertyWithValue("indexResolver", indexResolver); + } + + @Test + void customIndexResolverConfigurationWithProvidedMongoSessionConverter() { + + this.context = new AnnotationConfigApplicationContext(); + this.context.register(CustomIndexResolverConfigurationWithProvidedtMongoSessionConverter.class); + this.context.refresh(); + + ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); + IndexResolver indexResolver = this.context.getBean(IndexResolver.class); + + assertThat(repository).isNotNull(); + assertThat(indexResolver).isNotNull(); + assertThat(repository) + .extracting("mongoSessionConverter") + .hasFieldOrPropertyWithValue("indexResolver", indexResolver); + } + + @Test + void importConfigAndCustomize() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(ImportConfigAndCustomizeConfiguration.class); + this.context.refresh(); + ReactiveMongoSessionRepository sessionRepository = this.context.getBean(ReactiveMongoSessionRepository.class); + assertThat(sessionRepository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ZERO); + } + + @Test + void registerWhenSessionIdGeneratorBeanThenUses() { + registerAndRefresh(GoodConfig.class, SessionIdGeneratorConfiguration.class); + ReactiveMongoSessionRepository sessionRepository = this.context.getBean(ReactiveMongoSessionRepository.class); + assertThat(sessionRepository).extracting("sessionIdGenerator").isInstanceOf(TestSessionIdGenerator.class); + } + + @Test + void registerWhenNoSessionIdGeneratorBeanThenDefault() { + registerAndRefresh(GoodConfig.class); + ReactiveMongoSessionRepository sessionRepository = this.context.getBean(ReactiveMongoSessionRepository.class); + assertThat(sessionRepository).extracting("sessionIdGenerator").isInstanceOf(UuidSessionIdGenerator.class); + } + + private void registerAndRefresh(Class... annotatedClasses) { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(annotatedClasses); + this.context.refresh(); + } + + /** + * Reflectively extract the {@link AbstractMongoSessionConverter} from the {@link ReactiveMongoSessionRepository}. + * This is to avoid expanding the surface area of the API. + */ + private AbstractMongoSessionConverter findMongoSessionConverter(ReactiveMongoSessionRepository repository) { + + Field field = ReflectionUtils.findField(ReactiveMongoSessionRepository.class, "mongoSessionConverter"); + ReflectionUtils.makeAccessible(field); + try { + return (AbstractMongoSessionConverter) field.get(repository); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + + /** A configuration with all the right parts. */ + @Configuration(proxyBeanMethods = false) + @EnableMongoWebSession + static class GoodConfig { + + @Bean + ReactiveMongoOperations operations() { + return mock(ReactiveMongoOperations.class); + } + } + + /** A configuration where no {@link ReactiveMongoOperations} is defined. It's BAD! */ + @Configuration(proxyBeanMethods = false) + @EnableMongoWebSession + static class BadConfig {} + + @Configuration(proxyBeanMethods = false) + @EnableMongoWebSession + static class OverrideSessionConverterConfig { + + @Bean + ReactiveMongoOperations operations() { + return mock(ReactiveMongoOperations.class); + } + + @Bean + AbstractMongoSessionConverter mongoSessionConverter() { + return new JacksonMongoSessionConverter(); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableMongoWebSession(maxInactiveIntervalInSeconds = 123, collectionName = "test-case") + static class OverrideMongoParametersConfig { + + @Bean + ReactiveMongoOperations operations() { + return mock(ReactiveMongoOperations.class); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableMongoWebSession + static class ConfigWithReactiveAndImperativeMongoOperations { + + @Bean + ReactiveMongoOperations reactiveMongoOperations() { + return mock(ReactiveMongoOperations.class); + } + + @Bean + IndexOperations indexOperations() { + + IndexOperations indexOperations = mock(IndexOperations.class); + given(indexOperations.getIndexInfo()).willReturn(Collections.emptyList()); + return indexOperations; + } + + @Bean + MongoOperations mongoOperations(IndexOperations indexOperations) { + + MongoOperations mongoOperations = mock(MongoOperations.class); + given(mongoOperations.indexOps((String) any())).willReturn(indexOperations); + return mongoOperations; + } + } + + @Configuration(proxyBeanMethods = false) + @EnableSpringWebSession + static class CustomizedReactiveConfiguration extends ReactiveMongoWebSessionConfiguration { + + CustomizedReactiveConfiguration() { + + this.setCollectionName("custom-collection"); + this.setMaxInactiveInterval(Duration.ofSeconds(123)); + } + + @Bean + ReactiveMongoOperations reactiveMongoOperations() { + return mock(ReactiveMongoOperations.class); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableMongoWebSession + static class SessionRepositoryCustomizerConfiguration { + + @Bean + ReactiveMongoOperations operations() { + return mock(ReactiveMongoOperations.class); + } + + @Bean + @Order(0) + ReactiveSessionRepositoryCustomizer sessionRepositoryCustomizerOne() { + return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); + } + + @Bean + @Order(1) + ReactiveSessionRepositoryCustomizer sessionRepositoryCustomizerTwo() { + return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(10000)); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableMongoWebSession + static class CustomIndexResolverConfigurationWithDefaultMongoSessionConverter { + + @Bean + ReactiveMongoOperations operations() { + return mock(ReactiveMongoOperations.class); + } + + @Bean + @SuppressWarnings("unchecked") + IndexResolver indexResolver() { + return mock(IndexResolver.class); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableMongoWebSession + static class CustomIndexResolverConfigurationWithProvidedtMongoSessionConverter { + + @Bean + ReactiveMongoOperations operations() { + return mock(ReactiveMongoOperations.class); + } + + @Bean + JacksonMongoSessionConverter jacksonMongoSessionConverter() { + return new JacksonMongoSessionConverter(); + } + + @Bean + @SuppressWarnings("unchecked") + IndexResolver indexResolver() { + return mock(IndexResolver.class); + } + } + + @Configuration(proxyBeanMethods = false) + @Import(ReactiveMongoWebSessionConfiguration.class) + static class ImportConfigAndCustomizeConfiguration { + + @Bean + ReactiveMongoOperations operations() { + return mock(ReactiveMongoOperations.class); + } + + @Bean + ReactiveSessionRepositoryCustomizer sessionRepositoryCustomizer() { + return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); + } + } + + @Configuration(proxyBeanMethods = false) + static class SessionIdGeneratorConfiguration { + + @Bean + SessionIdGenerator sessionIdGenerator() { + return new TestSessionIdGenerator(); + } + } + + static class TestSessionIdGenerator implements SessionIdGenerator { + + @Override + public String generate() { + return "test"; + } + } +} diff --git a/src/test/java/org/springframework/session/data/mongo/AbstractMongoSessionConverterTests.java b/src/test/java/org/springframework/session/data/mongo/AbstractMongoSessionConverterTests.java deleted file mode 100644 index 78054f9..0000000 --- a/src/test/java/org/springframework/session/data/mongo/AbstractMongoSessionConverterTests.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.time.Duration; - -import com.mongodb.DBObject; -import org.bson.Document; -import org.junit.jupiter.api.Test; - -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextImpl; -import org.springframework.session.FindByIndexNameSessionRepository; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Greg Turnquist - */ -public abstract class AbstractMongoSessionConverterTests { - - abstract AbstractMongoSessionConverter getMongoSessionConverter(); - - @Test - void verifyRoundTripSerialization() throws Exception { - - // given - MongoSession toSerialize = new MongoSession(); - toSerialize.setAttribute("username", "john_the_springer"); - - // when - DBObject dbObject = convertToDBObject(toSerialize); - MongoSession deserialized = convertToSession(dbObject); - - // then - assertThat(deserialized).usingRecursiveComparison().isEqualTo(toSerialize); - } - - @Test - void verifyRoundTripSecuritySerialization() { - - // given - MongoSession toSerialize = new MongoSession(); - String principalName = "john_the_springer"; - SecurityContextImpl context = new SecurityContextImpl(); - context.setAuthentication(new UsernamePasswordAuthenticationToken(principalName, null)); - toSerialize.setAttribute("SPRING_SECURITY_CONTEXT", context); - - // when - DBObject serialized = convertToDBObject(toSerialize); - MongoSession deserialized = convertToSession(serialized); - - // then - assertThat(deserialized).usingRecursiveComparison().isEqualTo(toSerialize); - - SecurityContextImpl springSecurityContextBefore = toSerialize.getAttribute("SPRING_SECURITY_CONTEXT"); - SecurityContextImpl springSecurityContextAfter = deserialized.getAttribute("SPRING_SECURITY_CONTEXT"); - - assertThat(springSecurityContextBefore).usingRecursiveComparison().isEqualTo(springSecurityContextAfter); - assertThat(springSecurityContextAfter.getAuthentication().getPrincipal()).isEqualTo("john_the_springer"); - assertThat(springSecurityContextAfter.getAuthentication().getCredentials()).isNull(); - } - - @Test - void shouldExtractPrincipalNameFromAttributes() throws Exception { - - // given - MongoSession toSerialize = new MongoSession(); - String principalName = "john_the_springer"; - toSerialize.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName); - - // when - DBObject dbObject = convertToDBObject(toSerialize); - - // then - assertThat(dbObject.get("principal")).isEqualTo(principalName); - } - - @Test - void shouldExtractPrincipalNameFromAuthentication() throws Exception { - - // given - MongoSession toSerialize = new MongoSession(); - String principalName = "john_the_springer"; - SecurityContextImpl context = new SecurityContextImpl(); - context.setAuthentication(new UsernamePasswordAuthenticationToken(principalName, null)); - toSerialize.setAttribute("SPRING_SECURITY_CONTEXT", context); - - // when - DBObject dbObject = convertToDBObject(toSerialize); - - // then - assertThat(dbObject.get("principal")).isEqualTo(principalName); - } - - @Test - void sessionWrapperWithNoMaxIntervalShouldFallbackToDefaultValues() { - - // given - MongoSession toSerialize = new MongoSession(); - DBObject dbObject = convertToDBObject(toSerialize); - Document document = new Document(dbObject.toMap()); - document.remove("interval"); - - // when - MongoSession convertedSession = getMongoSessionConverter().convert(document); - - // then - assertThat(convertedSession.getMaxInactiveInterval()).isEqualTo(Duration.ofMinutes(30)); - } - - @Nullable - MongoSession convertToSession(DBObject session) { - return (MongoSession) getMongoSessionConverter().convert(session, TypeDescriptor.valueOf(DBObject.class), - TypeDescriptor.valueOf(MongoSession.class)); - } - - @Nullable - DBObject convertToDBObject(MongoSession session) { - return (DBObject) getMongoSessionConverter().convert(session, TypeDescriptor.valueOf(MongoSession.class), - TypeDescriptor.valueOf(DBObject.class)); - } - -} diff --git a/src/test/java/org/springframework/session/data/mongo/JacksonMongoSessionConverterTests.java b/src/test/java/org/springframework/session/data/mongo/JacksonMongoSessionConverterTests.java deleted file mode 100644 index 26efdbe..0000000 --- a/src/test/java/org/springframework/session/data/mongo/JacksonMongoSessionConverterTests.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.lang.reflect.Field; -import java.util.Date; -import java.util.HashMap; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.mongodb.DBObject; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.AssertionsForClassTypes; -import org.bson.Document; -import org.bson.types.ObjectId; -import org.junit.jupiter.api.Test; - -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.util.ReflectionUtils; - -/** - * @author Jakub Kubrynski - * @author Greg Turnquist - */ -class JacksonMongoSessionConverterTests extends AbstractMongoSessionConverterTests { - - JacksonMongoSessionConverter mongoSessionConverter = new JacksonMongoSessionConverter(); - - @Override - AbstractMongoSessionConverter getMongoSessionConverter() { - return this.mongoSessionConverter; - } - - @Test - void shouldSaveIdField() { - - // given - MongoSession session = new MongoSession(); - - // when - DBObject convert = this.mongoSessionConverter.convert(session); - - // then - AssertionsForClassTypes.assertThat(convert.get("_id")).isEqualTo(session.getId()); - AssertionsForClassTypes.assertThat(convert.get("id")).isNull(); - } - - @Test - void shouldQueryAgainstAttribute() throws Exception { - - // when - Query cart = this.mongoSessionConverter.getQueryForIndex("cart", "my-cart"); - - // then - AssertionsForClassTypes.assertThat(cart.getQueryObject().get("attrs.cart")).isEqualTo("my-cart"); - } - - @Test - void shouldAllowCustomObjectMapper() { - - // given - ObjectMapper myMapper = new ObjectMapper(); - - // when - JacksonMongoSessionConverter converter = new JacksonMongoSessionConverter(myMapper); - - // then - Field objectMapperField = ReflectionUtils.findField(JacksonMongoSessionConverter.class, "objectMapper"); - ReflectionUtils.makeAccessible(objectMapperField); - ObjectMapper converterMapper = (ObjectMapper) ReflectionUtils.getField(objectMapperField, converter); - - AssertionsForClassTypes.assertThat(converterMapper).isEqualTo(myMapper); - } - - @Test - void shouldNotAllowNullObjectMapperToBeInjected() { - - Assertions.assertThatIllegalArgumentException() - .isThrownBy(() -> new JacksonMongoSessionConverter((ObjectMapper) null)); - } - - @Test - void shouldSaveExpireAtAsDate() { - - // given - MongoSession session = new MongoSession(); - - // when - DBObject convert = this.mongoSessionConverter.convert(session); - - // then - AssertionsForClassTypes.assertThat(convert.get("expireAt")).isInstanceOf(Date.class); - AssertionsForClassTypes.assertThat(convert.get("expireAt")).isEqualTo(session.getExpireAt()); - } - - @Test - void shouldLoadExpireAtFromDocument() { - - // given - Date now = new Date(); - HashMap data = new HashMap<>(); - - data.put("expireAt", now); - data.put("@class", MongoSession.class.getName()); - data.put("_id", new ObjectId().toString()); - - Document document = new Document(data); - - // when - MongoSession convertedSession = this.mongoSessionConverter.convert(document); - - // then - AssertionsForClassTypes.assertThat(convertedSession).isNotNull(); - AssertionsForClassTypes.assertThat(convertedSession.getExpireAt()).isEqualTo(now); - } - -} diff --git a/src/test/java/org/springframework/session/data/mongo/MongoIndexedSessionRepositoryTests.java b/src/test/java/org/springframework/session/data/mongo/MongoIndexedSessionRepositoryTests.java deleted file mode 100644 index cb86171..0000000 --- a/src/test/java/org/springframework/session/data/mongo/MongoIndexedSessionRepositoryTests.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.time.Duration; -import java.time.Instant; -import java.util.Collections; -import java.util.Map; -import java.util.UUID; - -import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; -import org.bson.Document; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.session.FindByIndexNameSessionRepository; -import org.springframework.session.MapSession; -import org.springframework.session.SessionIdGenerator; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.verify; - -/** - * Tests for {@link MongoIndexedSessionRepository}. - * - * @author Jakub Kubrynski - * @author Vedran Pavic - * @author Greg Turnquist - */ -@ExtendWith(MockitoExtension.class) -class MongoIndexedSessionRepositoryTests { - - @Mock - private AbstractMongoSessionConverter converter; - - @Mock - private MongoOperations mongoOperations; - - private MongoIndexedSessionRepository repository; - - @BeforeEach - void setUp() { - - this.repository = new MongoIndexedSessionRepository(this.mongoOperations); - this.repository.setMongoSessionConverter(this.converter); - } - - @Test - void shouldCreateSession() { - - // when - MongoSession session = this.repository.createSession(); - - // then - assertThat(session.getId()).isNotEmpty(); - assertThat(session.getMaxInactiveInterval().getSeconds()) - .isEqualTo(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); - } - - @Test - void shouldSaveSession() { - - // given - MongoSession session = new MongoSession(); - BasicDBObject dbSession = new BasicDBObject(); - - given(this.converter.convert(session, TypeDescriptor.valueOf(MongoSession.class), - TypeDescriptor.valueOf(DBObject.class))) - .willReturn(dbSession); - // when - this.repository.save(session); - - // then - verify(this.mongoOperations).save(dbSession, MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME); - } - - @Test - void shouldGetSession() { - - // given - String sessionId = UUID.randomUUID().toString(); - Document sessionDocument = new Document(); - - given(this.mongoOperations.findById(sessionId, Document.class, - MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)) - .willReturn(sessionDocument); - - MongoSession session = new MongoSession(); - - given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class))) - .willReturn(session); - - // when - MongoSession retrievedSession = this.repository.findById(sessionId); - - // then - assertThat(retrievedSession).isEqualTo(session); - } - - @Test - void shouldHandleExpiredSession() { - - // given - String sessionId = UUID.randomUUID().toString(); - Document sessionDocument = new Document(); - - given(this.mongoOperations.findById(sessionId, Document.class, - MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)) - .willReturn(sessionDocument); - - MongoSession session = mock(MongoSession.class); - - given(session.isExpired()).willReturn(true); - given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class))) - .willReturn(session); - given(session.getId()).willReturn("sessionId"); - - // when - this.repository.findById(sessionId); - - // then - verify(this.mongoOperations).remove(any(Document.class), - eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)); - } - - @Test - void shouldDeleteSession() { - - // given - String sessionId = UUID.randomUUID().toString(); - - Document sessionDocument = new Document(); - sessionDocument.put("id", sessionId); - - MongoSession mongoSession = new MongoSession(sessionId, MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); - - given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class))) - .willReturn(mongoSession); - given(this.mongoOperations.findById(eq(sessionId), eq(Document.class), - eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME))) - .willReturn(sessionDocument); - - // when - this.repository.deleteById(sessionId); - - // then - verify(this.mongoOperations).remove(any(Document.class), - eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)); - } - - @Test - void shouldGetSessionsMapByPrincipal() { - - // given - String principalNameIndexName = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; - - Document document = new Document(); - - given(this.converter.getQueryForIndex(anyString(), any(Object.class))).willReturn(mock(Query.class)); - given(this.mongoOperations.find(any(Query.class), eq(Document.class), - eq(MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME))) - .willReturn(Collections.singletonList(document)); - - String sessionId = UUID.randomUUID().toString(); - - MongoSession session = new MongoSession(sessionId, 1800); - - given(this.converter.convert(document, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class))) - .willReturn(session); - - // when - Map sessionsMap = this.repository.findByIndexNameAndIndexValue(principalNameIndexName, - "john"); - - // then - assertThat(sessionsMap).containsOnlyKeys(sessionId); - assertThat(sessionsMap).containsValues(session); - } - - @Test - void shouldReturnEmptyMapForNotSupportedIndex() { - - // given - String index = "some_not_supported_index_name"; - - // when - Map sessionsMap = this.repository.findByIndexNameAndIndexValue(index, "some_value"); - - // then - assertThat(sessionsMap).isEmpty(); - } - - @Test - void createSessionWhenSessionIdGeneratorThenUses() { - this.repository.setSessionIdGenerator(new FixedSessionIdGenerator("123")); - MongoSession session = this.repository.createSession(); - assertThat(session.getId()).isEqualTo("123"); - assertThat(session.changeSessionId()).isEqualTo("123"); - } - - @Test - void setSessionIdGeneratorWhenNullThenThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSessionIdGenerator(null)); - } - - @Test - void findByIdWhenChangeSessionIdThenUsesSessionIdGenerator() { - this.repository.setSessionIdGenerator(new FixedSessionIdGenerator("456")); - - Document sessionDocument = new Document(); - - given(this.mongoOperations.findById("123", Document.class, - MongoIndexedSessionRepository.DEFAULT_COLLECTION_NAME)) - .willReturn(sessionDocument); - - MongoSession session = new MongoSession("123"); - - given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class))) - .willReturn(session); - - MongoSession retrievedSession = this.repository.findById("123"); - assertThat(retrievedSession.getId()).isEqualTo("123"); - String newSessionId = retrievedSession.changeSessionId(); - assertThat(newSessionId).isEqualTo("456"); - } - - @Test - void createSessionWhenMaxInactiveIntervalSetThenUse() { - this.repository.setDefaultMaxInactiveInterval(Duration.ofSeconds(60)); - MongoSession session = this.repository.createSession(); - Instant now = Instant.now(); - assertThat(session.getExpireAt()).isBetween(now.plusSeconds(59), Instant.now().plusSeconds(61)); - } - - static class FixedSessionIdGenerator implements SessionIdGenerator { - - private final String id; - - FixedSessionIdGenerator(String id) { - this.id = id; - } - - @Override - public String generate() { - return this.id; - } - - } - -} diff --git a/src/test/java/org/springframework/session/data/mongo/ReactiveMongoSessionRepositoryTests.java b/src/test/java/org/springframework/session/data/mongo/ReactiveMongoSessionRepositoryTests.java deleted file mode 100644 index 313f64c..0000000 --- a/src/test/java/org/springframework/session/data/mongo/ReactiveMongoSessionRepositoryTests.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo; - -import java.time.Duration; -import java.time.Instant; -import java.util.UUID; - -import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; -import com.mongodb.client.result.DeleteResult; -import org.bson.Document; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.ReactiveMongoOperations; -import org.springframework.data.mongodb.core.index.IndexOperations; -import org.springframework.session.MapSession; -import org.springframework.session.events.SessionDeletedEvent; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.BDDMockito.any; -import static org.mockito.BDDMockito.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.times; -import static org.mockito.BDDMockito.verify; - -/** - * Tests for {@link ReactiveMongoSessionRepository}. - * - * @author Jakub Kubrynski - * @author Vedran Pavic - * @author Greg Turnquist - */ -@ExtendWith(MockitoExtension.class) -class ReactiveMongoSessionRepositoryTests { - - @Mock - private AbstractMongoSessionConverter converter; - - @Mock - private ReactiveMongoOperations mongoOperations; - - @Mock - private MongoOperations blockingMongoOperations; - - @Mock - private ApplicationEventPublisher eventPublisher; - - private ReactiveMongoSessionRepository repository; - - @BeforeEach - void setUp() { - - this.repository = new ReactiveMongoSessionRepository(this.mongoOperations); - this.repository.setMongoSessionConverter(this.converter); - this.repository.setApplicationEventPublisher(this.eventPublisher); - } - - @Test - void shouldCreateSession() { - - this.repository.createSession() // - .as(StepVerifier::create) // - .expectNextMatches((mongoSession) -> { - assertThat(mongoSession.getId()).isNotEmpty(); - assertThat(mongoSession.getMaxInactiveInterval()) - .isEqualTo(Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS)); - return true; - }) // - .verifyComplete(); - } - - @Test - void shouldSaveSession() { - - // given - MongoSession session = new MongoSession(); - BasicDBObject dbSession = new BasicDBObject(); - - given(this.converter.convert(session, TypeDescriptor.valueOf(MongoSession.class), - TypeDescriptor.valueOf(DBObject.class))) - .willReturn(dbSession); - - given(this.mongoOperations.save(dbSession, "sessions")).willReturn(Mono.just(dbSession)); - - // when - this.repository.save(session) // - .as(StepVerifier::create) // - .verifyComplete(); - - verify(this.mongoOperations).save(dbSession, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME); - } - - @Test - void shouldGetSession() { - - // given - String sessionId = UUID.randomUUID().toString(); - Document sessionDocument = new Document(); - - given(this.mongoOperations.findById(sessionId, Document.class, - ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) - .willReturn(Mono.just(sessionDocument)); - - MongoSession session = new MongoSession(); - - given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class))) - .willReturn(session); - - // when - this.repository.findById(sessionId) // - .as(StepVerifier::create) // - .expectNext(session) // - .verifyComplete(); - } - - @Test - void shouldHandleExpiredSession() { - - // given - String sessionId = UUID.randomUUID().toString(); - Document sessionDocument = new Document(); - - given(this.mongoOperations.findById(sessionId, Document.class, - ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) - .willReturn(Mono.just(sessionDocument)); - - given(this.mongoOperations.remove(sessionDocument, ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) - .willReturn(Mono.just(DeleteResult.acknowledged(1))); - - MongoSession session = mock(MongoSession.class); - - given(session.isExpired()).willReturn(true); - given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class))) - .willReturn(session); - - // when - this.repository.findById(sessionId) // - .as(StepVerifier::create) // - .verifyComplete(); - - // then - verify(this.mongoOperations).remove(any(Document.class), - eq(ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)); - } - - @Test - void shouldDeleteSession() { - - // given - String sessionId = UUID.randomUUID().toString(); - Document sessionDocument = new Document(); - - given(this.mongoOperations.findById(sessionId, Document.class, - ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) - .willReturn(Mono.just(sessionDocument)); - - given(this.mongoOperations.remove(sessionDocument, "sessions")) - .willReturn(Mono.just(DeleteResult.acknowledged(1))); - - MongoSession session = mock(MongoSession.class); - - given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class))) - .willReturn(session); - - // when - this.repository.deleteById(sessionId) // - .as(StepVerifier::create) // - .verifyComplete(); - - verify(this.mongoOperations).remove(any(Document.class), - eq(ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)); - - verify(this.eventPublisher).publishEvent(any(SessionDeletedEvent.class)); - } - - @Test - void shouldInvokeMethodToCreateIndexesImperatively() { - - // given - IndexOperations indexOperations = mock(IndexOperations.class); - given(this.blockingMongoOperations.indexOps((String) any())).willReturn(indexOperations); - - this.repository.setBlockingMongoOperations(this.blockingMongoOperations); - - // when - this.repository.afterPropertiesSet(); - - // then - verify(this.blockingMongoOperations, times(1)).indexOps((String) any()); - verify(this.converter, times(1)).ensureIndexes(indexOperations); - } - - @Test - void createSessionWhenSessionIdGeneratorThenUses() { - this.repository.setSessionIdGenerator(() -> "test"); - - this.repository.createSession().as(StepVerifier::create).assertNext((mongoSession) -> { - assertThat(mongoSession.getId()).isEqualTo("test"); - assertThat(mongoSession.changeSessionId()).isEqualTo("test"); - }).verifyComplete(); - } - - @Test - void setSessionIdGeneratorWhenNullThenThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSessionIdGenerator(null)) - .withMessage("sessionIdGenerator cannot be null"); - } - - @Test - void findByIdWhenChangeSessionIdThenUsesSessionIdGenerator() { - this.repository.setSessionIdGenerator(() -> "test"); - - String sessionId = UUID.randomUUID().toString(); - Document sessionDocument = new Document(); - - given(this.mongoOperations.findById(sessionId, Document.class, - ReactiveMongoSessionRepository.DEFAULT_COLLECTION_NAME)) - .willReturn(Mono.just(sessionDocument)); - - MongoSession session = new MongoSession(sessionId); - - given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class), - TypeDescriptor.valueOf(MongoSession.class))) - .willReturn(session); - - this.repository.findById(sessionId).as(StepVerifier::create).assertNext((mongoSession) -> { - String oldId = mongoSession.getId(); - String newId = mongoSession.changeSessionId(); - assertThat(oldId).isEqualTo(sessionId); - assertThat(newId).isEqualTo("test"); - }).verifyComplete(); - } - - @Test - void createSessionWhenMaxInactiveIntervalSetThenUse() { - this.repository.setDefaultMaxInactiveInterval(Duration.ofSeconds(60)); - MongoSession session = this.repository.createSession().block(); - Instant now = Instant.now(); - assertThat(session.getExpireAt()).isBetween(now.plusSeconds(59), Instant.now().plusSeconds(61)); - } - -} diff --git a/src/test/java/org/springframework/session/data/mongo/config/annotation/web/http/MongoHttpSessionConfigurationTests.java b/src/test/java/org/springframework/session/data/mongo/config/annotation/web/http/MongoHttpSessionConfigurationTests.java deleted file mode 100644 index eb46469..0000000 --- a/src/test/java/org/springframework/session/data/mongo/config/annotation/web/http/MongoHttpSessionConfigurationTests.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo.config.annotation.web.http; - -import java.net.UnknownHostException; -import java.time.Duration; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.UnsatisfiedDependencyException; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.index.IndexOperations; -import org.springframework.mock.env.MockEnvironment; -import org.springframework.session.IndexResolver; -import org.springframework.session.Session; -import org.springframework.session.SessionIdGenerator; -import org.springframework.session.UuidSessionIdGenerator; -import org.springframework.session.config.SessionRepositoryCustomizer; -import org.springframework.session.data.mongo.AbstractMongoSessionConverter; -import org.springframework.session.data.mongo.JacksonMongoSessionConverter; -import org.springframework.session.data.mongo.MongoIndexedSessionRepository; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; - -/** - * Tests for {@link MongoHttpSessionConfiguration}. - * - * @author Eddú Meléndez - * @author Vedran Pavic - */ -class MongoHttpSessionConfigurationTests { - - private static final String COLLECTION_NAME = "testSessions"; - - private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600; - - private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - - @AfterEach - void after() { - - if (this.context != null) { - this.context.close(); - } - } - - @Test - void noMongoOperationsConfiguration() { - - assertThatExceptionOfType(UnsatisfiedDependencyException.class) - .isThrownBy(() -> registerAndRefresh(EmptyConfiguration.class)) - .withMessageContaining("mongoSessionRepository"); - } - - @Test - void defaultConfiguration() { - - registerAndRefresh(DefaultConfiguration.class); - - assertThat(this.context.getBean(MongoIndexedSessionRepository.class)).isNotNull(); - } - - @Test - void customCollectionName() { - - registerAndRefresh(CustomCollectionNameConfiguration.class); - - MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); - - assertThat(repository).isNotNull(); - assertThat(ReflectionTestUtils.getField(repository, "collectionName")).isEqualTo(COLLECTION_NAME); - } - - @Test - void setCustomCollectionName() { - - registerAndRefresh(CustomCollectionNameSetConfiguration.class); - - MongoHttpSessionConfiguration session = this.context.getBean(MongoHttpSessionConfiguration.class); - - assertThat(session).isNotNull(); - assertThat(ReflectionTestUtils.getField(session, "collectionName")).isEqualTo(COLLECTION_NAME); - } - - @Test - void customMaxInactiveIntervalInSeconds() { - - registerAndRefresh(CustomMaxInactiveIntervalInSecondsConfiguration.class); - - MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); - - assertThat(repository).extracting("defaultMaxInactiveInterval") - .isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); - } - - @Test - void setCustomMaxInactiveIntervalInSeconds() { - - registerAndRefresh(CustomMaxInactiveIntervalInSecondsSetConfiguration.class); - - MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); - - assertThat(repository).extracting("defaultMaxInactiveInterval") - .isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); - } - - @Test - void setCustomSessionConverterConfiguration() { - - registerAndRefresh(CustomSessionConverterConfiguration.class); - - MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); - AbstractMongoSessionConverter mongoSessionConverter = this.context.getBean(AbstractMongoSessionConverter.class); - - assertThat(repository).isNotNull(); - assertThat(mongoSessionConverter).isNotNull(); - assertThat(ReflectionTestUtils.getField(repository, "mongoSessionConverter")).isEqualTo(mongoSessionConverter); - } - - @Test - void resolveCollectionNameByPropertyPlaceholder() { - - this.context - .setEnvironment(new MockEnvironment().withProperty("session.mongo.collectionName", COLLECTION_NAME)); - registerAndRefresh(CustomMongoJdbcSessionConfiguration.class); - - MongoHttpSessionConfiguration configuration = this.context.getBean(MongoHttpSessionConfiguration.class); - - assertThat(ReflectionTestUtils.getField(configuration, "collectionName")).isEqualTo(COLLECTION_NAME); - } - - @Test - void sessionRepositoryCustomizer() { - - registerAndRefresh(MongoConfiguration.class, SessionRepositoryCustomizerConfiguration.class); - - MongoIndexedSessionRepository sessionRepository = this.context.getBean(MongoIndexedSessionRepository.class); - - assertThat(sessionRepository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ofSeconds(10000)); - } - - @Test - void customIndexResolverConfigurationWithDefaultMongoSessionConverter() { - - registerAndRefresh(MongoConfiguration.class, - CustomIndexResolverConfigurationWithDefaultMongoSessionConverter.class); - - MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); - IndexResolver indexResolver = this.context.getBean(IndexResolver.class); - - assertThat(repository).isNotNull(); - assertThat(indexResolver).isNotNull(); - assertThat(repository).extracting("mongoSessionConverter") - .hasFieldOrPropertyWithValue("indexResolver", indexResolver); - } - - @Test - void customIndexResolverConfigurationWithProvidedMongoSessionConverter() { - - registerAndRefresh(MongoConfiguration.class, - CustomIndexResolverConfigurationWithProvidedMongoSessionConverter.class); - - MongoIndexedSessionRepository repository = this.context.getBean(MongoIndexedSessionRepository.class); - IndexResolver indexResolver = this.context.getBean(IndexResolver.class); - - assertThat(repository).isNotNull(); - assertThat(indexResolver).isNotNull(); - assertThat(repository).extracting("mongoSessionConverter") - .hasFieldOrPropertyWithValue("indexResolver", indexResolver); - } - - @Test - void importConfigAndCustomize() { - registerAndRefresh(ImportConfigAndCustomizeConfiguration.class); - MongoIndexedSessionRepository sessionRepository = this.context.getBean(MongoIndexedSessionRepository.class); - assertThat(sessionRepository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ZERO); - } - - @Test - void registerWhenSessionIdGeneratorBeanThenUses() { - registerAndRefresh(SessionIdGeneratorConfiguration.class); - MongoIndexedSessionRepository sessionRepository = this.context.getBean(MongoIndexedSessionRepository.class); - assertThat(sessionRepository).extracting("sessionIdGenerator").isInstanceOf(TestSessionIdGenerator.class); - } - - @Test - void registerWhenNoSessionIdGeneratorBeanThenDefault() { - registerAndRefresh(DefaultConfiguration.class); - MongoIndexedSessionRepository sessionRepository = this.context.getBean(MongoIndexedSessionRepository.class); - assertThat(sessionRepository).extracting("sessionIdGenerator").isInstanceOf(UuidSessionIdGenerator.class); - } - - private void registerAndRefresh(Class... annotatedClasses) { - - this.context.register(annotatedClasses); - this.context.refresh(); - } - - @Configuration - @EnableMongoHttpSession - static class EmptyConfiguration { - - } - - static class BaseConfiguration { - - @Bean - MongoOperations mongoOperations() throws UnknownHostException { - - MongoOperations mongoOperations = mock(MongoOperations.class); - IndexOperations indexOperations = mock(IndexOperations.class); - - given(mongoOperations.indexOps(anyString())).willReturn(indexOperations); - - return mongoOperations; - } - - } - - @Configuration - @EnableMongoHttpSession - static class DefaultConfiguration extends BaseConfiguration { - - } - - @Configuration - static class MongoConfiguration extends BaseConfiguration { - - } - - @Configuration - @EnableMongoHttpSession(collectionName = COLLECTION_NAME) - static class CustomCollectionNameConfiguration extends BaseConfiguration { - - } - - @Configuration - @Import(MongoConfiguration.class) - static class CustomCollectionNameSetConfiguration extends MongoHttpSessionConfiguration { - - CustomCollectionNameSetConfiguration() { - setCollectionName(COLLECTION_NAME); - } - - } - - @Configuration - @EnableMongoHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS) - static class CustomMaxInactiveIntervalInSecondsConfiguration extends BaseConfiguration { - - } - - @Configuration - @Import(MongoConfiguration.class) - static class CustomMaxInactiveIntervalInSecondsSetConfiguration extends MongoHttpSessionConfiguration { - - CustomMaxInactiveIntervalInSecondsSetConfiguration() { - setMaxInactiveInterval(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); - } - - } - - @Configuration - @Import(MongoConfiguration.class) - static class CustomSessionConverterConfiguration extends MongoHttpSessionConfiguration { - - @Bean - AbstractMongoSessionConverter mongoSessionConverter() { - return mock(AbstractMongoSessionConverter.class); - } - - } - - @Configuration - @EnableMongoHttpSession(collectionName = "${session.mongo.collectionName}") - static class CustomMongoJdbcSessionConfiguration extends BaseConfiguration { - - @Bean - PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { - return new PropertySourcesPlaceholderConfigurer(); - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableMongoHttpSession - static class SessionRepositoryCustomizerConfiguration { - - @Bean - @Order(0) - SessionRepositoryCustomizer sessionRepositoryCustomizerOne() { - return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); - } - - @Bean - @Order(1) - SessionRepositoryCustomizer sessionRepositoryCustomizerTwo() { - return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(10000)); - } - - } - - @Configuration - @EnableMongoHttpSession - static class CustomIndexResolverConfigurationWithDefaultMongoSessionConverter { - - @Bean - @SuppressWarnings("unchecked") - IndexResolver indexResolver() { - return mock(IndexResolver.class); - } - - } - - @Configuration - @EnableMongoHttpSession - static class CustomIndexResolverConfigurationWithProvidedMongoSessionConverter { - - @Bean - AbstractMongoSessionConverter mongoSessionConverter() { - return new JacksonMongoSessionConverter(); - } - - @Bean - @SuppressWarnings("unchecked") - IndexResolver indexResolver() { - return mock(IndexResolver.class); - } - - } - - @Configuration(proxyBeanMethods = false) - @Import(MongoHttpSessionConfiguration.class) - static class ImportConfigAndCustomizeConfiguration extends BaseConfiguration { - - @Bean - SessionRepositoryCustomizer sessionRepositoryCustomizer() { - return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableMongoHttpSession - @Import(MongoConfiguration.class) - static class SessionIdGeneratorConfiguration { - - @Bean - SessionIdGenerator sessionIdGenerator() { - return new TestSessionIdGenerator(); - } - - } - - static class TestSessionIdGenerator implements SessionIdGenerator { - - @Override - public String generate() { - return "test"; - } - - } - -} diff --git a/src/test/java/org/springframework/session/data/mongo/config/annotation/web/reactive/ReactiveMongoWebSessionConfigurationTests.java b/src/test/java/org/springframework/session/data/mongo/config/annotation/web/reactive/ReactiveMongoWebSessionConfigurationTests.java deleted file mode 100644 index 1a18bfe..0000000 --- a/src/test/java/org/springframework/session/data/mongo/config/annotation/web/reactive/ReactiveMongoWebSessionConfigurationTests.java +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Copyright 2014-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.mongo.config.annotation.web.reactive; - -import java.lang.reflect.Field; -import java.time.Duration; -import java.util.Collections; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.UnsatisfiedDependencyException; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.ReactiveMongoOperations; -import org.springframework.data.mongodb.core.index.IndexOperations; -import org.springframework.session.IndexResolver; -import org.springframework.session.ReactiveSessionRepository; -import org.springframework.session.Session; -import org.springframework.session.SessionIdGenerator; -import org.springframework.session.UuidSessionIdGenerator; -import org.springframework.session.config.ReactiveSessionRepositoryCustomizer; -import org.springframework.session.config.annotation.web.server.EnableSpringWebSession; -import org.springframework.session.data.mongo.AbstractMongoSessionConverter; -import org.springframework.session.data.mongo.JacksonMongoSessionConverter; -import org.springframework.session.data.mongo.JdkMongoSessionConverter; -import org.springframework.session.data.mongo.ReactiveMongoSessionRepository; -import org.springframework.util.ReflectionUtils; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import org.springframework.web.server.session.WebSessionManager; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.BDDMockito.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.times; -import static org.mockito.BDDMockito.verify; - -/** - * Verify various configurations through {@link EnableSpringWebSession}. - * - * @author Greg Turnquist - * @author Vedran Pavic - */ -class ReactiveMongoWebSessionConfigurationTests { - - private AnnotationConfigApplicationContext context; - - @AfterEach - void tearDown() { - - if (this.context != null) { - this.context.close(); - } - } - - @Test - void enableSpringWebSessionConfiguresThings() { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(GoodConfig.class); - this.context.refresh(); - - WebSessionManager webSessionManagerFoundByType = this.context.getBean(WebSessionManager.class); - Object webSessionManagerFoundByName = this.context.getBean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME); - - assertThat(webSessionManagerFoundByType).isNotNull(); - assertThat(webSessionManagerFoundByName).isNotNull(); - assertThat(webSessionManagerFoundByType).isEqualTo(webSessionManagerFoundByName); - - assertThat(this.context.getBean(ReactiveSessionRepository.class)).isNotNull(); - } - - @Test - void missingReactorSessionRepositoryBreaksAppContext() { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(BadConfig.class); - - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(this.context::refresh) - .withMessageContaining("Error creating bean with name 'reactiveMongoSessionRepository'") - .withMessageContaining("No qualifying bean of type '" + ReactiveMongoOperations.class.getCanonicalName()); - } - - @Test - void defaultSessionConverterShouldBeJdkWhenOnClasspath() throws IllegalAccessException { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(GoodConfig.class); - this.context.refresh(); - - ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); - - AbstractMongoSessionConverter converter = findMongoSessionConverter(repository); - - assertThat(converter).isOfAnyClassIn(JdkMongoSessionConverter.class); - } - - @Test - void overridingMongoSessionConverterWithBeanShouldWork() throws IllegalAccessException { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(OverrideSessionConverterConfig.class); - this.context.refresh(); - - ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); - - AbstractMongoSessionConverter converter = findMongoSessionConverter(repository); - - assertThat(converter).isOfAnyClassIn(JacksonMongoSessionConverter.class); - } - - @Test - void overridingIntervalAndCollectionNameThroughAnnotationShouldWork() { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(OverrideMongoParametersConfig.class); - this.context.refresh(); - - ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); - - assertThat(repository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ofSeconds(123)); - assertThat(repository).extracting("collectionName").isEqualTo("test-case"); - } - - @Test - void reactiveAndBlockingMongoOperationsShouldEnsureIndexing() { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(ConfigWithReactiveAndImperativeMongoOperations.class); - this.context.refresh(); - - MongoOperations operations = this.context.getBean(MongoOperations.class); - IndexOperations indexOperations = this.context.getBean(IndexOperations.class); - - verify(operations, times(1)).indexOps((String) any()); - verify(indexOperations, times(1)).getIndexInfo(); - verify(indexOperations, times(1)).ensureIndex(any()); - } - - @Test - void overrideCollectionAndInactiveIntervalThroughConfigurationOptions() { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(CustomizedReactiveConfiguration.class); - this.context.refresh(); - - ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); - - assertThat(repository.getCollectionName()).isEqualTo("custom-collection"); - assertThat(repository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ofSeconds(123)); - } - - @Test - void sessionRepositoryCustomizer() { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(SessionRepositoryCustomizerConfiguration.class); - this.context.refresh(); - - ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); - - assertThat(repository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ofSeconds(10000)); - } - - @Test - void customIndexResolverConfigurationWithDefaultMongoSessionConverter() { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(CustomIndexResolverConfigurationWithDefaultMongoSessionConverter.class); - this.context.refresh(); - - ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); - IndexResolver indexResolver = this.context.getBean(IndexResolver.class); - - assertThat(repository).isNotNull(); - assertThat(indexResolver).isNotNull(); - assertThat(repository).extracting("mongoSessionConverter") - .hasFieldOrPropertyWithValue("indexResolver", indexResolver); - } - - @Test - void customIndexResolverConfigurationWithProvidedMongoSessionConverter() { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(CustomIndexResolverConfigurationWithProvidedtMongoSessionConverter.class); - this.context.refresh(); - - ReactiveMongoSessionRepository repository = this.context.getBean(ReactiveMongoSessionRepository.class); - IndexResolver indexResolver = this.context.getBean(IndexResolver.class); - - assertThat(repository).isNotNull(); - assertThat(indexResolver).isNotNull(); - assertThat(repository).extracting("mongoSessionConverter") - .hasFieldOrPropertyWithValue("indexResolver", indexResolver); - } - - @Test - void importConfigAndCustomize() { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(ImportConfigAndCustomizeConfiguration.class); - this.context.refresh(); - ReactiveMongoSessionRepository sessionRepository = this.context.getBean(ReactiveMongoSessionRepository.class); - assertThat(sessionRepository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ZERO); - } - - @Test - void registerWhenSessionIdGeneratorBeanThenUses() { - registerAndRefresh(GoodConfig.class, SessionIdGeneratorConfiguration.class); - ReactiveMongoSessionRepository sessionRepository = this.context.getBean(ReactiveMongoSessionRepository.class); - assertThat(sessionRepository).extracting("sessionIdGenerator").isInstanceOf(TestSessionIdGenerator.class); - } - - @Test - void registerWhenNoSessionIdGeneratorBeanThenDefault() { - registerAndRefresh(GoodConfig.class); - ReactiveMongoSessionRepository sessionRepository = this.context.getBean(ReactiveMongoSessionRepository.class); - assertThat(sessionRepository).extracting("sessionIdGenerator").isInstanceOf(UuidSessionIdGenerator.class); - } - - private void registerAndRefresh(Class... annotatedClasses) { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(annotatedClasses); - this.context.refresh(); - } - - /** - * Reflectively extract the {@link AbstractMongoSessionConverter} from the - * {@link ReactiveMongoSessionRepository}. This is to avoid expanding the surface area - * of the API. - */ - private AbstractMongoSessionConverter findMongoSessionConverter(ReactiveMongoSessionRepository repository) { - - Field field = ReflectionUtils.findField(ReactiveMongoSessionRepository.class, "mongoSessionConverter"); - ReflectionUtils.makeAccessible(field); - try { - return (AbstractMongoSessionConverter) field.get(repository); - } - catch (IllegalAccessException ex) { - throw new RuntimeException(ex); - } - } - - /** - * A configuration with all the right parts. - */ - @Configuration(proxyBeanMethods = false) - @EnableMongoWebSession - static class GoodConfig { - - @Bean - ReactiveMongoOperations operations() { - return mock(ReactiveMongoOperations.class); - } - - } - - /** - * A configuration where no {@link ReactiveMongoOperations} is defined. It's BAD! - */ - @Configuration(proxyBeanMethods = false) - @EnableMongoWebSession - static class BadConfig { - - } - - @Configuration(proxyBeanMethods = false) - @EnableMongoWebSession - static class OverrideSessionConverterConfig { - - @Bean - ReactiveMongoOperations operations() { - return mock(ReactiveMongoOperations.class); - } - - @Bean - AbstractMongoSessionConverter mongoSessionConverter() { - return new JacksonMongoSessionConverter(); - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableMongoWebSession(maxInactiveIntervalInSeconds = 123, collectionName = "test-case") - static class OverrideMongoParametersConfig { - - @Bean - ReactiveMongoOperations operations() { - return mock(ReactiveMongoOperations.class); - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableMongoWebSession - static class ConfigWithReactiveAndImperativeMongoOperations { - - @Bean - ReactiveMongoOperations reactiveMongoOperations() { - return mock(ReactiveMongoOperations.class); - } - - @Bean - IndexOperations indexOperations() { - - IndexOperations indexOperations = mock(IndexOperations.class); - given(indexOperations.getIndexInfo()).willReturn(Collections.emptyList()); - return indexOperations; - } - - @Bean - MongoOperations mongoOperations(IndexOperations indexOperations) { - - MongoOperations mongoOperations = mock(MongoOperations.class); - given(mongoOperations.indexOps((String) any())).willReturn(indexOperations); - return mongoOperations; - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableSpringWebSession - static class CustomizedReactiveConfiguration extends ReactiveMongoWebSessionConfiguration { - - CustomizedReactiveConfiguration() { - - this.setCollectionName("custom-collection"); - this.setMaxInactiveInterval(Duration.ofSeconds(123)); - } - - @Bean - ReactiveMongoOperations reactiveMongoOperations() { - return mock(ReactiveMongoOperations.class); - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableMongoWebSession - static class SessionRepositoryCustomizerConfiguration { - - @Bean - ReactiveMongoOperations operations() { - return mock(ReactiveMongoOperations.class); - } - - @Bean - @Order(0) - ReactiveSessionRepositoryCustomizer sessionRepositoryCustomizerOne() { - return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); - } - - @Bean - @Order(1) - ReactiveSessionRepositoryCustomizer sessionRepositoryCustomizerTwo() { - return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(10000)); - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableMongoWebSession - static class CustomIndexResolverConfigurationWithDefaultMongoSessionConverter { - - @Bean - ReactiveMongoOperations operations() { - return mock(ReactiveMongoOperations.class); - } - - @Bean - @SuppressWarnings("unchecked") - IndexResolver indexResolver() { - return mock(IndexResolver.class); - } - - } - - @Configuration(proxyBeanMethods = false) - @EnableMongoWebSession - static class CustomIndexResolverConfigurationWithProvidedtMongoSessionConverter { - - @Bean - ReactiveMongoOperations operations() { - return mock(ReactiveMongoOperations.class); - } - - @Bean - JacksonMongoSessionConverter jacksonMongoSessionConverter() { - return new JacksonMongoSessionConverter(); - } - - @Bean - @SuppressWarnings("unchecked") - IndexResolver indexResolver() { - return mock(IndexResolver.class); - } - - } - - @Configuration(proxyBeanMethods = false) - @Import(ReactiveMongoWebSessionConfiguration.class) - static class ImportConfigAndCustomizeConfiguration { - - @Bean - ReactiveMongoOperations operations() { - return mock(ReactiveMongoOperations.class); - } - - @Bean - ReactiveSessionRepositoryCustomizer sessionRepositoryCustomizer() { - return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); - } - - } - - @Configuration(proxyBeanMethods = false) - static class SessionIdGeneratorConfiguration { - - @Bean - SessionIdGenerator sessionIdGenerator() { - return new TestSessionIdGenerator(); - } - - } - - static class TestSessionIdGenerator implements SessionIdGenerator { - - @Override - public String generate() { - return "test"; - } - - } - -}